65.9K
CodeProject 正在变化。 阅读更多。
Home

将增强现实添加到基于 ML Kit 的 Android 应用

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (6投票s)

2019年6月27日

CPOL
viewsIcon

13108

在本篇文章中,我们将探讨如何利用应用中已有的 ML 功能来添加增强现实 (AR) 功能,以便在拍照时将红色嘴唇图像叠加在人嘴上。

机器学习 (ML) 正在迅速成为日常计算机交互中的常见功能,而我们甚至都没有意识到。一个例子是像“使用 Google ML Kit 在 Android 上进行面部检测”一文介绍的应用程序那样的,基于 ML 的图像识别。正如该文章所示,由于 Google 最近推出的名为 ML Kit 的新 Firebase SDK 等项目,ML 现在甚至对独立开发者也触手可及。

在上篇文章中,我们创建了一个示例应用程序,该应用程序可以拍照,将其交给 ML Kit 进行面部检测,并使用 ML Kit 返回的数据勾勒出面部特征。

在本篇文章中,我们将探讨如何利用应用中已有的 ML 功能来添加增强现实 (AR) 功能,以便在拍照时将红色嘴唇图像叠加在人嘴上。

为了创建此应用程序,我们将继续使用上一篇文章中创建的示例应用程序。如果您还没有机会阅读上一篇文章,请务必阅读它并创建示例应用程序以便在此处进行跟进。

入门

为了回顾上一篇文章所完成的工作,我们创建了一个可以拍摄用户照片的应用程序。然后,使用 Google 的 ML Kit SDK,它可以检测照片中是否存在人脸,并提供一些额外信息,例如面部特征的位置。利用这些信息,我们在图像上绘制了检测到的面部特征的叠加层。

在本文中,我们将在此基础上继续学习,并了解如何根据从 ML Kit 获取的信息添加 AR 功能。

为了实现这一目标,我们将创建另一个按钮,它将使用一个类似于 ML Kit 文章中创建的 Graphic 类,在用户脸部上方创建一个图像叠加层。我们将查看显示一张普通嘴唇图片,我们还将查看设置颜色滤镜以获得不同颜色嘴唇。

我们将使用为本教程第一部分已创建的 Android Studio 项目和 Firebase 注册信息。

获取 AR 叠加资源

在开始深入研究代码之前,我们首先需要开始寻找可以使用的资源。对于商业应用程序,我们很可能会聘请设计师制作精美有趣的图像。在我们的例子中,我们正在制作一个简单的应用程序来演示如何将图像叠加在我们自己的图像之上。我使用了来自 pixabay.com 的两个口红资源。首先,我们将使用 带有牙齿的红唇

我们还将使用 红唇印

对于第一张图片,我们将直接将资源应用于嘴唇。该资源将在应用程序中称为 **lip**。

对于第二张图片,我们将做一些更有趣的事情。我们将对图像应用颜色滤镜,以用不同的颜色重新使用它。该资源将在应用程序中称为 lip_filter

理解要使用的 ML Kit API

要将我们的嘴唇叠加层添加到用户的嘴唇上,我们需要使用 ML Kit 的面部地标识别来定位用户嘴唇的位置。

为了实现这一点,我们需要从 ML Kit 返回的 FirebaseVisionFace 对象中查找嘴唇。此对象存储了用户脸部的所有面部信息。我们将要使用的 地标MOUTH_BOTTOM

根据 API,MOUTH_BOTTOM 提供:“主体下唇的中心”。

与之前的例子不同,这次我们得到了一个接近我们想要在画布上附加图像的位置,但它并不是我们想要的精确位置。我们需要做一些工作来确保我们将资源放置在正确的位置。

编写代码

这次,当我们完成时,结果将类似于

或者,使用我们的唇色滤镜,像这样

为了让用户能够添加这些嘴唇,我们将更改现有应用程序的布局。我们将

  1. 创建一个新的(目前禁用的)按钮,该按钮允许我们在检测到的人脸上绘制口红。
  2. 创建一个提供颜色选项的 spinner,允许用户选择他们想要的口红颜色。

为了创建我们的布局,这是我们新的activity_main.xml 的样子

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

    <TextView
            android:layout_width="188dp"
            android:layout_height="wrap_content"
            android:id="@+id/happiness"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="82dp"
            android:layout_alignParentStart="true"/>
    <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:layout_editor_absoluteY="27dp"
            tools:layout_editor_absoluteX="78dp"
            android:id="@+id/imageView"/>
    <com.example.mlkittutorial.GraphicOverlay
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/graphicOverlay"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            android:layout_marginStart="0dp"
            android:layout_marginTop="0dp"/>
    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
                  android:layout_alignParentStart="true" android:layout_alignParentBottom="true">
        <Button
                android:text="Take Picture"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="takePicture"
                android:id="@+id/takePicture"
                android:visibility="visible"
                android:enabled="true"/>
        <Button
                android:text="Detect Face"
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/detectFace"
                android:onClick="detectFace"
                android:visibility="visible"
                android:enabled="false"/>
        <Button
                android:text="Draw Lip"
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/drawLip"
                android:onClick="drawLip"
                android:visibility="visible"
                android:enabled="false"/>
    </LinearLayout>
    <Spinner
            android:layout_width="145dp"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_marginEnd="0dp"
            android:layout_alignParentBottom="true"
            android:id="@+id/colorSpinner"
            android:layout_marginBottom="78dp"/>
</RelativeLayout>

在 Android Studio 中是这样的

这是更新后的MainActivity.kt ,用于使用我们的新选项

package com.example.mlkittutorial

import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import android.widget.ArrayAdapter
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import com.google.firebase.ml.vision.face.FirebaseVisionFace
import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    private val requestImageCapture = 1
    private var cameraImage: Bitmap? = null
    private var faces: List<FirebaseVisionFace>? = null
    private var color = arrayOf("None", "Red", "Blue", "Green", "Yellow")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // setup the list of colors to show in our spinner
        val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, color)
        adapter.setDropDownViewResource(android.R.layout.simple_selectable_list_item)
        colorSpinner.adapter = adapter
    }

    /** Receive the result from the camera app */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == requestImageCapture && resultCode == RESULT_OK && data != null && data.extras != null) {
            val imageBitmap = data.extras.get("data") as Bitmap

            // Instead of creating a new file in the user's device to get a full scale image
            // resize our smaller imageBitMap to fit the screen
            val width = Resources.getSystem().displayMetrics.widthPixels
            val height = width / imageBitmap.width * imageBitmap.height
            cameraImage = Bitmap.createScaledBitmap(imageBitmap, width, height, false)

            // Display the image and enable our ML facial detection button
            imageView.setImageBitmap(cameraImage)
            detectFace.isEnabled = true
        }
    }

    /** Callback for the take picture button */
    fun takePicture(view: View) {
        // Take an image using an existing camera app
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takePictureIntent, requestImageCapture)
                happiness.text = ""
                graphicOverlay.clear()
            }
        }
    }

    /** Callback for the detect face button */
    fun detectFace(view: View) {
        // Build the options for face detector SDK
        if (cameraImage != null) {
            val image = FirebaseVisionImage.fromBitmap(cameraImage as Bitmap)
            val builder = FirebaseVisionFaceDetectorOptions.Builder()
            builder.setContourMode(FirebaseVisionFaceDetectorOptions.ALL_CONTOURS)
            builder.setClassificationMode(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS)
            builder.setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS) // different

            val options = builder.build()

            // Send our image to be detected by the SDK
            val detector = FirebaseVision.getInstance().getVisionFaceDetector(options)
            detector.detectInImage(image).addOnSuccessListener { faces ->
                displayImage(faces)
            }
        }
    }

    /** Draw a graphic overlay on top of our image */
    private fun displayImage(faces: List<FirebaseVisionFace>) {
        graphicOverlay.clear()
        if (faces.isNotEmpty()) {
            // We will only draw an overlay on the first face
            val firstFace = faces[0]
            val smilingChance = firstFace.smilingProbability * 100
            val faceGraphic = FaceContourGraphic(graphicOverlay, firstFace)
            graphicOverlay.add(faceGraphic)
            happiness.text = "Smile Probability: " + (smilingChance) + "%"

            // Save the face and enable the lip drawing button
            drawLip.isEnabled = true
            this.faces = faces
        } else {
            happiness.text = "No face detected"
        }
    }

    /** Draw a graphical lip on top of the user's lips */
    fun drawLip(view: View) {
        graphicOverlay.clear()
        if (faces != null) {
            val position = colorSpinner.selectedItemPosition

            // based off of the position of our item, we pick the matching color
            val color = when (position) {
                1 -> Color.RED
                2 -> Color.BLUE
                3 -> Color.GREEN
                4 -> Color.YELLOW
                else -> null
            }
            val bmp = when (position) {
                0 -> BitmapFactory.decodeResource(resources, R.drawable.lip)
                else -> BitmapFactory.decodeResource(resources, R.drawable.filter_lip)
            }

            // Iterate through all of detected faces to create a graphic overlay
            faces?.forEach {
                val faceGraphic = FaceLipGraphic(graphicOverlay, it, bmp, color)
                graphicOverlay.add(faceGraphic)
            }
        }
    }
}

如果您将代码与上一项目的代码进行比较,您会发现我们做了一些更改。

  1. OnCreate 中,我创建了一个列表适配器,它将用于显示可用的嘴唇颜色选项。
  2. detectFace 中,我启用了 landmarkMode,以便能够查询我们脸部相关的地标信息。
  3. displayImage 中,我们对代码进行了一些更改,以保存我们的 FireBaseVisionFace 对象。这样,如果我们运行检测,它将被存储起来,以便我们稍后可以使用它来在用户的图片上绘制口红,而不必再次检测。一旦运行了检测,我们还会启用绘制嘴唇按钮,并保存我们要在上面绘制口红的脸部列表。
  4. drawLip 中,它连接到我们的 drawLip 按钮,我们检查用户想从 spinner 中使用哪个嘴唇选项。根据该选项,我们将特定的图像资源加载到 bitmap 中,然后遍历我们的 faces 列表,最后将我们的 Bitmap 和颜色传递给我们新的 FaceLipGraphic 类来绘制口红。

我们还没有看到我们的新 FaceLipGraphic 类,但从宏观上看,它类似于 Firebase 提供的 FaceContourGraphic 类,因为我们在拍摄的照片上绘制东西。这是 FaceLipGraphic 类的样子

package com.example.mlkittutorial

import android.graphics.*
import com.google.firebase.ml.vision.face.FirebaseVisionFace
import com.google.firebase.ml.vision.face.FirebaseVisionFaceLandmark

/** Graphic instance for rendering face contours graphic overlay view.  */
class FaceLipGraphic(overlay: GraphicOverlay, private val firebaseVisionFace: FirebaseVisionFace?, private val lips: Bitmap, color: Int? = null)
    : GraphicOverlay.Graphic(overlay) {

    private val lipPaint: Paint = Paint()

    init {
        lipPaint.alpha = 70
        if (color != null) {
            lipPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
        }
    }

    /** Draws the lips on the position on the supplied canvas. */
    override fun draw(canvas: Canvas) {
        val face = firebaseVisionFace ?: return
        val mouth = face.getLandmark(FirebaseVisionFaceLandmark.MOUTH_BOTTOM) ?: return

        // Get the center position of the bottom lip
        val mouthX = mouth.position.x
        val mouthY = mouth.position.y

        // Calculate the ratio of the size of a mouth to a face to re-size the lip image we have to an ideal size. I found this number through trial and error.
        val idealWidth = (face.boundingBox.width() / 2.5).toInt()
        val idealHeight = (face.boundingBox.height() / 4)
        val lipImage = Bitmap.createScaledBitmap(lips, idealWidth, idealHeight, false)

        // Our lip image will start at the center of the bottom lip. To allow our image to be in the center
        // we need to move our image half of its width to the left to move the center of our lip to be on the
        // center of the user's lips.
        val lipPositionX = mouthX - idealWidth / 2

        // We need to create an offset to move our lips to the center. Currently we're at the center of the bottom of our lip.
        // The center of the bottom lip is 1/4 of the whole lip. If we want to find the real center of the mouth, we need
        // to move our height another 1/4 up.
        val bottomLipOffset = idealHeight / 4

        // Just like our width, we need to move our image's height position half of its height up to be at the center
        // of the bottom lip. We also move our image up by another 1/4 of the bottom lip size so that the image will be
        // at the center of the user's mouth
        val lipPositionY = mouthY - idealHeight / 2 - bottomLipOffset

        // Draw the lipstick into our canvas
        canvas.drawBitmap(lipImage, lipPositionX, lipPositionY, lipPaint)
    }
}

FaceLipGraphic 内部,我们进行计算来确定口红图像的位置,然后将其绘制到我们拍摄的图片上。

init 中, 我们设置了我们新初始化的 Paint 对象,我们将用它来将口红绘制到 Canvas 上。我们首先将 alpha 设置为 70,这使得我们的图像有些透明,以便可以看到对象下方的部分。

接下来,我们检查是否提供了非空颜色。如果有效,我们可以为我们的 Paint 对象设置一个颜色滤镜,它允许我们为我们的图像着色。我不会深入细节,但我选择使用 PorterDuffColorFilter,它允许我们“ 使用单一颜色和特定的 PorterDuff 来着色源像素。”我使用了 SRC_IN 作为我的 PorterDuff.Mode,因为它用我们的新颜色替换了我们现有的图像。

接下来,在 draw() 中,我们进行必要的计算,将口红图像放置在正确的位置。我在代码中关于计算推导出正确位置的计算添加了详细的注释。

对于不熟悉 Android 画布坐标的人来说,我的数学可能看起来错了,但我保证它是正确的。这里有一些额外的解释

一个正常的图表发生在第一象限,所以 (0,0) 位于左下角。然而,Android 的 Canvas 发生在第四象限,所以 (0, 0) 位于左上角。

这是一张图来表示坐标系

特别是,与大多数人通常假设的相反,在我们的 Y 轴上进行减法实际上是将我们的图像向上移动而不是向下移动。X 轴保持不变。有了这些知识,您应该能够理解我为确定需要绘制口红图像的坐标所做的计算。

结论

当您开始阅读这篇文章时,您可能认为在您的应用程序中使用 ML 和 AR 可能超出了您的能力范围。希望通过阅读本文和上一篇文章,您已经学会了如何使用 Google 的 ML Kit 来检测人脸和面部特征,并学会了如何利用这些信息为您的应用程序添加 AR 功能。现在,您应该对如何使用这项技术创造更多创新的应用程序更有信心。

使用 Google 的 ML Kit 的另一个好处是,由于我们手机内置的 Arm 处理器强大的处理能力,我们可以直接在设备上完成所有这些工作,而无需将任何数据发送到云端进行处理。

您已经看到了我们如何在用户的图像上应用口红图像,但这只是 ML 和 AR 可实现功能的冰山一角!如果您想继续学习如何扩展,看看如何为眼睛添加卡通眼睛,或者在鼻子上放一个有趣的鼻子。祝您好运,玩得开心!

© . All rights reserved.