ARCore 增强图像

ARCore 是一个用于在 Android 上构建增强现实应用的平台。借助增强图像,您可以创建能够识别预注册图像并在上面附加虚拟内容的 AR 应用。

本 Codelab 将指导您修改现有 ARCore 示例应用,以纳入移动或固定的增强图像。

构建内容

在本 Codelab 中,您将基于现有的 ARCore 示例应用构建您的应用。完成该 Codelab 时,您的应用将:

  • 能够检测图像目标并在目标上附加虚拟迷宫。示例视觉效果如下
  • 能够跟踪在视图中移动的目标

6bc6605df89de525.gif

这是您第一次构建 ARCore 应用吗?

您是打算在此 Codelab 中编写示例代码,还是只想阅读这些页面?

编写示例代码 只想阅读这些页面

学习内容

  • 如何在 Java 中使用 ARCore 中的增强图像。
  • 如何衡量图像被 ARCore 识别的难易程度。
  • 如何在图像上附加虚拟内容并跟踪其移动。

所需条件

在开始本 Codelab 之前,请确保您已准备好一切:

  • 受支持的 ARCore 设备,通过 USB 线连接到开发机器。
  • ARCore 1.9 或更高版本。此 APK 通常通过 Play 商店自动安装到设备上。如果您的设备上未安装所需版本的 ARCore,您可以随时从 Play 商店安装它
  • 安装了 Android Studio(v3.1 或更高版本)的开发机器。
  • 能够访问互联网,方便在开发过程中下载各类库。

现在,万事俱备,我们开始吧!

首先,从 GitHub 下载 ARCore Java SDK arcore-android-sdk-1.18.1.zip。将其解压缩到所需位置。解压文件夹的名称将为 arcore-android-sdk-1.18.1。

启动 Android Studio,然后点击 Open an existing Android Studio project

5fbf2b21609187cc.png

转到以下解压缩的文件夹:

arcore-android-sdk-1.18.1/samples/augmented_image_java

点击 Open

等待 Android Studio 完成项目同步。如果您的 Android Studio 未安装必要的组件,同步可能会失败,并返回“Install missing platform and sync project”(安装缺少的平台并同步项目)消息。按照相关说明解决该问题。

现在,您的 ARCore 应用项目已可以正常运行,让我们对它进行一次测试运行。

将 ARCore 设备连接到开发机器,然后使用菜单 Run > Run 'app' 在设备上运行调试版本。在显示的对话框中,系统会提示您选择要从哪个设备运行应用,

选择已连接的设备,然后点击 OK

1aa2c6faa7ecdbd0.png

92e4c144a632b4ca.png

此示例项目使用 targetSdkVersion 28。如果出现构建错误(如 Failed to find Build Tools revision 28.0.3),请按照 Android Studio 中的说明下载并安装所需的 Android 构建工具版本。

如果一切顺利,示例应用将在设备上启动,并提示您授权增强图像拍摄照片和视频。点按 ALLOW 授予权限。

我们为示例应用提供一张图像供其查看。

回到 Android Studio 中,在 Project 窗口中,导航到 app > assets,然后双击文件 default.jpg 将其打开。

9b333680e7b9f247.jpeg

将设备摄像头对准屏幕上显示的地球图像,然后按照说明将正在扫描的图像纳入对焦范围内。

图像边框会在图像之上叠加,如下所示:

999e05ed35964f6e.png

接下来,我们将对示例应用稍做改进。

正如我们在本 Codelab 开头部分所述,我们将在图像上附加一个迷宫小游戏。首先,我们在 poly.google.com 上找到一个迷宫模型,该模型包含许多根据 CC-BY 许可免费提供的 3D 模型。

在本 Codelab 中,我们将使用 Evol 提供的“Circle Maze - Green”模型,此模型根据 CC-BY 3.0 授权。

832fc0f1b09fea1e.png

按以下步骤下载此模型并将其导入 Android Studio:

  1. 转到模型的 Poly 页面
  2. 点击 Download,然后选择 OBJ File

系统将下载名为 green-maze.zip 的文件。

  1. 解压缩 green-maze.zip 并将其中的内容复制到以下位置:arcore-android-sdk-1.18.1/samples/augmented_image_java/app/src/main/assets/models/green-maze
  2. 在 Android Studio 中,导航到 app > assets > models > green-maze

此文件夹中应该有两个文件:GreenMaze.objGreenMaze.mtl

a1f33a2d2d407e03.png

接下来,我们将加载此 OBJ 文件,并将其显示在检测到的图像之上。

下载好 3D 模型 GreenMaze.obj 后,我们接下来将其来显示在图像之上。

  1. AugmentedImageRenderer.java 中,添加一个名为 mazeRenderer 的成员变量以呈现该迷宫模型。由于迷宫应附加到图像,因此需要将 mazeRenderer 放到 AugmentedImageRenderer 类中。
  2. createOnGlThread 函数中,加载 GreenMaze.obj。为简单起见,我们将使用同一种边框纹理作为其纹理。
  3. draw 函数中,根据检测到的图像的大小调整迷宫大小,然后绘制迷宫。

AugmentedImageRenderer.java 中,做出以下更改。

  // Add a member variable to hold the maze model.
  private final ObjectRenderer mazeRenderer = new ObjectRenderer();

  // Replace the definition of the createOnGlThread function with the
  // following code, which loads GreenMaze.obj.
  public void createOnGlThread(Context context) throws IOException {

    mazeRenderer.createOnGlThread(
        context, "models/green-maze/GreenMaze.obj", "models/frame_base.png");
    mazeRenderer.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);

  }

  // Replace the definition of the draw function with the
  // following code
  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {
    float[] tintColor =
        convertHexToColor(TINT_COLORS_HEX[augmentedImage.getIndex() % TINT_COLORS_HEX.length]);

    final float maze_edge_size = 492.65f; // Magic number of maze size
    final float max_image_edge = Math.max(augmentedImage.getExtentX(), augmentedImage.getExtentZ()); // Get largest detected image edge size

    Pose anchorPose = centerAnchor.getPose();

    float mazsScaleFactor = max_image_edge / maze_edge_size; // scale to set Maze to image size
    float[] modelMatrix = new float[16];

    // OpenGL Matrix operation is in the order: Scale, rotation and Translation
    // So the manual adjustment is after scale
    // The 251.3f and 129.0f is magic number from the maze obj file
    // We need to do this adjustment because the maze obj file
    // is not centered around origin. Normally when you
    // work with your own model, you don't have this problem.
    Pose mozeModelLocalOffset = Pose.makeTranslation(
                                -251.3f * mazsScaleFactor,
                                0.0f,
                                129.0f * mazsScaleFactor);
    anchorPose.compose(mozeModelLocalOffset).toMatrix(modelMatrix, 0);
    mazeRenderer.updateModelMatrix(modelMatrix, mazsScaleFactor, mazsScaleFactor/10.0f, mazsScaleFactor);
    mazeRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);
  }

完成上述代码更改后,我们就能在地球图像 default.jpg 上显示迷宫了。

我在上面的代码中使用了一些神奇的数字。别担心,使用它们只是因为我们不能完全控制此 3D 模型。于是我手动解析了该 obj 文件,以便了解模型的中心位置 (x, y, z) 及其大小。在本 Codelab 中,我们不打算介绍如何解析,因此我在这里直接给出值。迷宫模型的维度为 492.65 x 120 x 492.65,中心位置为 (251.3, 60, -129.0)。其顶点的 X、Y、Z 坐标值范围分别为 [5.02, 497.67]、[0, 120]、[-375.17, 117.25]。因此,我们需要按 image_size / 492.65 设置迷宫模型的比例。您可能已经注意到,该 3D 迷宫模型的中心位置不在原点 (0, 0, 0),因此我们需要手动引入偏移 mozeModelLocalOffset

此外,由于迷宫墙对于我们的 Codelab 来说仍然太高了,所以我们将它再缩小 1/10。这样可以使墙的高度降低,以便墙与墙的间隔更明显。为此,我们需要引入一个辅助函数,让我们能够对 X、Y、Z 坐标进行非平均缩放。

augmentedimage/rendering/ObjectRenderer.java 中,做出以下更改。

  public void updateModelMatrix(float[] modelMatrix, float scaleFactorX, float scaleFactorY, float scaleFactorZ) {
    float[] scaleMatrix = new float[16];
    Matrix.setIdentityM(scaleMatrix, 0);
    scaleMatrix[0] = scaleFactorX;
    scaleMatrix[5] = scaleFactorY;
    scaleMatrix[10] = scaleFactorZ;
    Matrix.multiplyMM(this.modelMatrix, 0, modelMatrix, 0, scaleMatrix, 0);
  }

好了,我们尝试在支持 ARCore 的设备上运行一下。现在,迷宫的大小应与图像的大小相同。

772cbe2a8baef3ba.png

现在,我们来添加一个在迷宫中移动的对象。在本 Codelab 中,为简单起见,我们就使用 ARCore Android SDK 中包含的 Android 人物 andy.obj 文件。此外,我们还将使用图像边框纹理作为人物的纹理,这样做便于从视觉上和图象上附加的绿色迷宫区分开。

将以下代码添加到 AugmentedImageNode.java 中。

// Add a private member to render andy
  private final ObjectRenderer andyRenderer = new ObjectRenderer();

  public void createOnGlThread(Context context) throws IOException {

    // Add initialization for andyRenderer at the end of the createOnGlThread function.
    andyRenderer.createOnGlThread(
        context, "models/andy.obj", "models/andy.png");
    andyRenderer.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
  }

  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {

    // In draw() function, at the end add code to display the Andy, standing on top of the maze
    Pose andyModelLocalOffset = Pose.makeTranslation(
        0.0f,
        0.1f,
        0.0f);
    anchorPose.compose(andyModelLocalOffset).toMatrix(modelMatrix, 0);
    andyRenderer.updateModelMatrix(modelMatrix, 0.05f); // 0.05f is a Magic number to scale
    andyRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);

  }

接下来,请尝试在设备上运行此应用。我们应该看到如下内容。

cb1e74569d7ace69.png

确定目标图像的画质

ARCore 依赖图像中的视觉特征来识别图像。并非所有图像的画质都一样,并且易于识别。

利用 ARCore Android SDK 中的 arcoreimg 工具,您可以验证目标图像的画质。我们可以运行这个命令行工具来确定图像对 ARCore 而言的识别难易程度。此工具会输出 0 到 100 之间的数字,100 表示最容易识别。示例如下:

arcore-android-sdk-1.18.1/tools/arcoreimg/macos$
$ ./arcoreimg  eval-img --input_image_path=/Users/username/maze.jpg
100

最后一部分实际上与 ARCore 无关,添加这部分只是为了给示例应用增添一些趣味。跳过此部分也无妨。

我们将使用开源物理引擎 jBullet 来处理物理模拟。

具体做法如下:

  1. 将 GreenMame.obj 添加到项目资产目录,以便我们在运行时加载该对象。
  2. 创建 PhysicsController 类,以管理所有物理相关函数。在内部,它使用 JBullet 物理引擎。
  3. 识别到图像后调用 PhysicsController,然后再调用 updatePhysics
  4. 利用真实重力在迷宫中移动球。请注意,我们必须适当缩放球的大小,以便它能够在迷宫中穿行。

下载 PhysicsController.java 代码,并将其添加到项目中的 arcore-android-sdk-1.18.1/samples/augmented_image_java/app/src/main/java/com/google/ar/core/examples/java/augmentedimage/ 目录中

然后在现有的 java 代码中做出如下更改。如下所示,

在 Android Studio 中,将 GreenMaze.obj

app > assets > models > green-maze

复制到:

app > assets

app/build.gradle 中,添加以下代码。

    // Add these dependencies.
    implementation 'cz.advel.jbullet:jbullet:20101010-1'

    // Obj - a simple Wavefront OBJ file loader
    // https://github.com/javagl/Obj
    implementation 'de.javagl:obj:0.2.1'

AugmentedImageRenderer.java 中,添加以下代码。

// Add this line at the top with the rest of the imports.
  private Pose andyPose = Pose.IDENTITY;

  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {

    // Use these code to replace previous code for rendering the Andy object
    // Adjust andy's rendering position
    // Andy's pose is at Maze's vertex's coordinate
    Pose andyPoseInImageSpace = Pose.makeTranslation(
        andyPose.tx() * mazsScaleFactor,
        andyPose.ty() * mazsScaleFactor,
        andyPose.tz() * mazsScaleFactor);

    anchorPose.compose(andyPoseInImageSpace).toMatrix(modelMatrix, 0);
    andyRenderer.updateModelMatrix(modelMatrix, 0.05f);
    andyRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);
  }

  // Add a new utility function to receive Andy pose updates
  public void updateAndyPose(Pose pose) {
    andyPose = pose;
  }

AugmentedImageActivity.java 中,添加以下代码。

import com.google.ar.core.Pose;

  // Declare the PhysicsController class.
  private PhysicsController physicsController;

  // Update the case clause for TRACKING as below
  private void drawAugmentedImages(

    ...

        case TRACKING:
          // Have to switch to UI Thread to update View.
          this.runOnUiThread(
              new Runnable() {
                @Override
                public void run() {
                  fitToScanView.setVisibility(View.GONE);
                }
              });

          // Create a new anchor for newly found images.
          if (!augmentedImageMap.containsKey(augmentedImage.getIndex())) {
            Anchor centerPoseAnchor = augmentedImage.createAnchor(augmentedImage.getCenterPose());
            augmentedImageMap.put(
                augmentedImage.getIndex(), Pair.create(augmentedImage, centerPoseAnchor));

            physicsController = new PhysicsController(this);
          } else {
            Pose ballPose = physicsController.getBallPose();
            augmentedImageRenderer.updateAndyPose(ballPose);

            // Use real world gravity, (0, -10, 0) as gravity
            // Convert to Physics world coordinate (because Maze mesh has to be static)
            // Use it as a force to move the ball
            Pose worldGravityPose = Pose.makeTranslation(0, -10f, 0);
            Pose mazeGravityPose = augmentedImage.getCenterPose().inverse().compose(worldGravityPose);
            float mazeGravity[] = mazeGravityPose.getTranslation();
            physicsController.applyGravityToBall(mazeGravity);

            physicsController.updatePhysics();
          }
          break;

这样我们就能让球像下图所示那样移动了。

2f0df284705d3704.gif

开心地游戏吧!

恭喜!您已完成了本 Codelab。我们来回顾一下在本 Codelab 中学到的内容。

  • 构建并运行了 ARCore AugmentedImage Java 示例。
  • 更新了示例,以便对近处的图像自动对焦,并使图像边框随图像大小调整。
  • 更新了示例,以将用户指定的图像作为目标。
  • 更新了示例,以适当的比例在图像上显示迷宫模型。
  • 利用图像的不同摆放方式做了一些有趣的尝试。

如果您想查看完整代码,请点击此处下载代码。

这个 Codelab 学起来是否有趣?

在完成本 Codelab 的过程中,您是否学到了有用的东西?

您是否在本 Codelab 中完成了应用的构建?

您是否打算在未来 6 个月之内制作一款 ARCore 应用?

可能