在 Android 上调用 Vision API Product Search 后端

1. 准备工作

bd8c01b2f8013c6d.png

您看过 Google 智能镜头演示视频吗?您在那里可以将手机摄像头对准某个物体,并找到在线购买物品的位置。如果您想要了解如何向应用添加同一功能,此 Codelab 很适合您。它是学习衔接课程的一部分,将向您介绍如何在移动应用中构建产品图片搜索功能。

在此 Codelab 中,您将学习如何从移动应用调用使用 Vision API Product Search 构建的后端。此后端可以获取查询图片,从产品目录中搜索外观类似的产品。

您可以了解构建可视化产品搜索功能的其余步骤,包括如何使用机器学习套件对象检测和跟踪检测查询图片中的对象,并让用户选择要搜索哪个产品(在学习衔接课程中)。

您将构建的内容

  • 在此 Codelab 中,您将从可以检测输入图片中的对象的 Android 应用入手。您将编写代码来选取用户选择的对象,将其发送到产品搜索后端,并在屏幕上显示搜索结果。
  • 最后,您应该会看到与右侧图片类似的内容。

学习内容

  • 如何从 Android 应用中调用和解析 Vision API Product Search API 的响应

所需条件

  • 最新版 Android Studio (v4.1.2+)
  • Android Studio 模拟器或一台 Android 实体设备
  • 示例代码
  • 使用 Kotlin 进行 Android 开发的基础知识

本 Codelab 主要介绍 Vision API Product Search。我们不讨论无关的概念和代码块,只提供给您复制和粘贴。

2. Vision API Product Search 简介

Vision API Product Search 是 Google Cloud 中的一项功能,可让用户从产品目录中搜索外观类似的产品。零售商可以创建产品,且每个产品都包含从一组视角直观描述产品的参考图片。然后,您可以将这些产品添加到产品组(即产品目录)。目前,Vision API Product Search 支持以下产品类别:家用品、服装、玩具、包装产品和一般产品。

用户使用自己的图片查询产品集时,Vision API Product Search 会应用机器学习,以比较用户查询图片中的产品与零售商产品集中的图片,然后返回在视觉和语义上类似的结果的排序列表。

3.下载并运行起始应用

下载代码

点击下面的链接可下载本 Codelab 的所有代码:

下载源代码

解压下载的 ZIP 文件。该操作将解压根文件夹 (odml-pathways-main),其中包含您需要的所有资源。对于此 Codelab,您只需要 product-search/codelab2/android 子目录中的源代码。

odml-pathways 代码库中的 codelab2 子目录包含两个目录:

  • android_studio_folder.pngstarter - 您为此 Codelab 构建的起始代码。
  • android_studio_folder.pngfinal - 完成后的示例应用的完整代码。

此处的起始应用正是您在检测图片中的对象以构建可视化产品搜索:Android Codelab 中构建的。它使用机器学习套件对象检测和跟踪功能来检测图片中的对象并将其显示在屏幕上。

将应用导入 Android Studio

首先,将 starter 应用导入 Android Studio。

转到 Android Studio,选择 Import Project (Gradle, Eclipse ADT, etc.),然后从之前下载的源代码中选择 starter 文件夹。

7c0f27882a2698ac.png

运行起始应用

现在,您已将项目导入 Android Studio,可以首次运行该应用了。通过 USB 将 Android 设备连接到主机,或启动 Android Studio 模拟器,然后点击 Android Studio 工具栏中的 Run 图标 (execute.png)。

(如果此按钮已停用,请确保仅导入 starter/app/build.gradle,而非整个代码库。)

现在,该应用应该已经在您的 Android 设备上启动了。它已经有对象检测功能:从图片中检测时尚物品,并向您显示这些物品的位置。请使用预设照片进行确认。

c6102a808fdfcb11.png

可检测图片中对象的起始应用屏幕截图

接下来,您将扩展该应用,将检测到的对象发送到 Vision API Product Search 后端,并在屏幕上显示搜索结果。

4.处理对象选择

允许用户点按检测到的对象进行选择

现在,您需要添加代码,以便用户从图片中选择对象并开始产品搜索。起始应用已能够检测图片中的对象。有可能图片中有多个对象,或者检测到的对象仅占图片的一小部分。因此,您需要让用户点按其中一个检测到的对象,以指明他们想要将哪个对象用于产品搜索。

9cdfcead6d95a87.png

从图片中检测到的时尚物品的屏幕截图

为了使此 Codelab 简单易行并专注于机器学习,起始应用中实现了一些 Android 样板代码,可帮助您检测用户点按的是哪个对象。在主 activity 中显示图像的视图 (ObjectDetectorActivity) 实际上是可扩展 Android 操作系统默认 ImageView 的自定义视图 (ImageClickableView)。它实现了一些便捷的实用程序方法,包括:

  • fun setOnObjectClickListener(listener: ((objectImage: Bitmap) -> Unit)) 这是一个回调,用于接收仅包含用户已点按的对象的剪裁后的图片。您需要将此剪裁后的图片发送到产品搜索后端。

添加代码以处理用户点按检测到的对象。

转到 initViews 方法(在 ObjectDetectorActivity 类,并在方法的末尾添加以下几行代码:(Android Studio 会告知您找不到 startProductImageSearch 方法。不用担心,稍后您将实现它。)

// Callback received when the user taps on any of the detected objects.
ivPreview.setOnObjectClickListener { objectImage ->
    startProductImageSearch(objectImage)
}

每当用户点按屏幕上检测到的任何对象时,系统都会调用 onObjectClickListener。它接收仅包含所选对象的剪裁后的图片。例如,如果用户点按右边穿连衣裙的人,系统将使用 objectImage 触发监听器,如下所示。

9cac8458d0f326e6.png

传递给 onObjectClickListener 的剪裁图片示例

将剪裁后的图片发送到产品搜索活动

现在,您将实现在单独的 activity (ProductSearchActivity) 中将查询图片发送到 Vision API Product Search 后端的逻辑。

所有界面组件都已提前实施,您可以专注于编写代码来与产品搜索后端进行通信。

25939f5a13eeb3c3.png

ProductSearchActivity 上的界面组件的屏幕截图

添加代码以将用户选择的对象图片发送到 ProductSearchActivity

返回 Android Studio,并将此 startProductImageSearch 方法添加到 ObjectDetectorActivity 类:

private fun startProductImageSearch(objectImage: Bitmap) {
    try {
        // Create file based Bitmap. We use PNG to preserve the image quality
        val savedFile = createImageFile(ProductSearchActivity.CROPPED_IMAGE_FILE_NAME)
        objectImage.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(savedFile))

        // Start the product search activity (using Vision Product Search API.).
        startActivity(
            Intent(
                    this,
                    ProductSearchActivity::class.java
            ).apply {
                // As the size limit of a bundle is 1MB, we need to save the bitmap to a file
                // and reload it in the other activity to support large query images.
                putExtra(
                    ProductSearchActivity.REQUEST_TARGET_IMAGE_PATH,
                    savedFile.absolutePath
                )
            })
    } catch (e: Exception) {
        // IO Exception, Out Of memory ....
        Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
        Log.e(TAG, "Error starting the product image search activity.", e)
    }
}

代码段会执行以下 3 步操作:

  • 获取剪裁后的图片并将其序列化为 PNG 文件。
  • 启动 ProductSearchActivity 以执行产品搜索序列。
  • 在 start-activity intent 中包含剪裁后的图片 URI,以便 ProductSearchActivity 日后可以检索它以用作查询图片。

以下是几点注意事项:

  • 用于检测对象和查询后端的逻辑已拆分为两个 activity,以使该 Codelab 更易于理解。您可以自行决定如何在应用中实现它们。
  • 您需要将查询图片写入文件并在 activity 之间传递图片 URI,因为查询图片可能大于 Android intent 的 1MB 大小限制。
  • 您可以将查询图片存储在 PNG 中,因为它是一种无损格式。

检索产品搜索活动中的查询图片

ProductSearchActivity 中,用于检索查询图片并将其显示在屏幕上的代码已在起始应用中实现。

转到 onCreate 方法并确认此代码已存在:

// Receive the query image and show it on the screen
intent.getStringExtra(REQUEST_TARGET_IMAGE_PATH)?.let { absolutePath ->
    viewBinding.ivQueryImage.setImageBitmap(BitmapFactory.decodeFile(absolutePath))
}

运行应用

现在,点击 Android Studio 工具栏中的 Run 图标 (execute.png)。

该应用加载后,点按任意预设图片,然后选择一个检测到的对象。

确认 ProductSearchActivity 与您点按的图片一起显示。目前,搜索按钮不会执行任何操作,但我们接下来将实现该按钮。

fed40f81b8b43801.png

点按某个检测到的对象后,您应该会看到类似屏幕。

5. 探索产品搜索后端

构建产品图片搜索后端

本 Codelab 需要一个使用 Vision API Product Search 构建的产品搜索后端。您可以通过以下两种方法实现此目的:

方法 1:使用为您部署的演示后端

您可以使用 Google 已为您部署的产品搜索后端继续此 Codelab。您可以按照 Vision API Product Search 快速入门复制演示后端。

方法 2:按照 Vision API Product Search 快速入门创建自己的后端

如果您希望深入了解如何构建产品搜索后端,以便日后为您自己的产品目录构建该产品后端,建议您使用此方法。您需要有:

  • 已启用结算功能的 Google Cloud 帐号。(可以是免费试用帐号。)
  • 了解 Google Cloud 相关概念,包括项目、服务帐号等。

您可以稍后在学习衔接课程中了解如何执行此操作。

了解重要概念

与产品搜索后端互动时,您会遇到以下概念:

  • 产品集:产品集是一组产品的简单容器。产品目录可表示为产品集及其产品。
  • 产品:创建产品集后,您可以创建产品并将它们添加到产品集。
  • 产品的参考图片:包含产品各种视图的图片。参考图片用于搜索外观相似的产品。
  • 搜索产品:创建产品集且将产品集编入索引后,您可以使用 Cloud Vision API 查询产品集。

了解预设产品目录

此 Codelab 中使用的产品搜索演示后端是使用 Vision API Product Search 和大约一百只鞋和连衣裙图片的产品目录创建的。以下是该目录中的一些图片:

4f1a8507b74ab178.png 79a5fc6c829eca77.png 3528c872f813826e.png

预设产品目录示例

调用产品搜索演示后端

您可以直接从移动应用中调用 Vision API Product Search,方法是设置 Google Cloud API 密钥并仅允许您的应用访问 API 密钥。

为使本 Codelab 保持简单,已设置代理端点,让您可以访问演示后端,而无需担心 API 密钥和身份验证。它接收来自移动应用的 HTTP 请求,附加 API 密钥,并将请求转发到 Vision API Product Search 后端。然后,代理接收来自后端的响应并将其返回到移动应用。

在此 Codelab 中,您将使用 Vision API Product Search 的两个 API:

6.实现 API 客户端

了解产品搜索工作流程

按照此工作流程使用后端执行产品搜索:

实现 API 客户端类

现在,您将实现代码,以便在名为 ProductSearchAPIClient 的专用类中调用产品搜索后端。我们已在起始应用中为您实现了一些样板代码:

  • class ProductSearchAPIClient:这个类现在通常为空,但它有一些您将在此 Codelab 中实现的方法。
  • fun convertBitmapToBase64(bitmap: Bitmap):将位图实例转换为 Base64 表示法以发送到产品搜索后端
  • fun annotateImage(image: Bitmap): Task<List<ProductSearchResult>>:调用 projects.locations.images.annotate API 并解析响应。
  • fun fetchReferenceImage(searchResult: ProductSearchResult): Task<ProductSearchResult>:调用 projects.locations.products.referenceImages.get API 并解析响应。
  • SearchResult.kt:此文件包含多个数据类,用于表示 Vision API Product Search 后端返回的类型。

指定 API 配置

转到 ProductSearchAPIClient 类,您将看到产品搜索后端的一些配置已经定义:

// Define the product search backend
// Option 1: Use the demo project that we have already deployed for you
const val VISION_API_URL =
    "https://us-central1-odml-codelabs.cloudfunctions.net/productSearch"
const val VISION_API_KEY = ""
const val VISION_API_PROJECT_ID = "odml-codelabs"
const val VISION_API_LOCATION_ID = "us-east1"
const val VISION_API_PRODUCT_SET_ID = "product_set0"
  • VISION_API_URL 是 Cloud Vision API 的 API 端点。继续演示后端时,请将此值设置为代理端点。但是,如果您部署自己的后端,则需要将其更改为 Cloud Vision API 端点。https://vision.googleapis.com/v1
  • VISION_API_KEY 是您的 Cloud 项目的 API 密钥。由于代理已处理身份验证,因此您可以将此字段留空。
  • VISION_API_PROJECT_ID 是 Cloud 项目 ID。odml-codelabs 是部署演示后端的 Cloud 项目。
  • VISION_API_LOCATION_ID 是部署产品搜索后端的 Cloud 位置。us-east1 是部署演示后端的位置。
  • VISION_API_PRODUCT_SET_ID 是您要在其中搜索外观相似的产品的产品目录(即 Vision API 术语中的“product set”)ID。一个 Cloud 项目中可以有多个目录。product_set0 是演示后端的预设产品目录。

7. 调用 Product Search API

探索 API 请求和响应格式

您可以将图片的 Google Cloud Storage URI、网址或 base64 编码字符串传递给 Vision API Product Search,从而找到与指定图片类似的产品。在本 Codelab 中,您将使用 base64 编码的字符串选项,因为我们的查询图片只存在于用户的设备上。

您需要使用以下 JSON 正文向 projects.locations.images.annotate 端点发送 POST 请求:

{
  "requests": [
    {
      "image": {
        "content": {base64-encoded-image}
      },
      "features": [
        {
          "type": "PRODUCT_SEARCH",
          "maxResults": 5
        }
      ],
      "imageContext": {
        "productSearchParams": {
          "productSet": "projects/{project-id}/locations/{location-id}/productSets/{product-set-id}",
          "productCategories": [
               "apparel-v2"
          ],
        }
      }
    }
  ]
}

您需要指定一些参数:

  • base64-encoded-image:查询图片的二进制数据的 base64 表示(ASCII 字符串)。
  • project-id:您的 GCP 项目 ID。
  • location-id:有效的位置标识符。
  • product-set-id:要对其执行操作的产品集的 ID。

由于您的产品目录仅包含鞋子和连衣裙图片,因此请将 productCategories 指定为 apparel-v2。这里的 v2 表示我们使用服饰产品搜索机器学习模型的第 2 版。

如果请求成功,服务器将返回一个 200 OK HTTP 状态代码以及 JSON 格式的响应:响应 JSON 包含以下两种结果类型:

  • productSearchResults - 包含整个图片的匹配产品列表。
  • productGroupedResults - 包含图片中识别的每个产品的边界框坐标和匹配项。

由于产品是从原始图片中剪裁出来的,因此您会在 productSearchResults 列表中解析结果。

以下是产品搜索结果对象中的一些重要字段:

  • product.name:产品的唯一标识符,格式为 projects/{project-id}/locations/{location-id}/products/{product_id}
  • product.score:一个值,用于指示搜索结果与查询图片的相似程度。值越高,相似度就越高。
  • product.image:产品参考图片的唯一标识符,格式为 projects/{project-id}/locations/{location-id}/products/{product_id}/referenceImages/{image_id}。您需要向 projects.locations.products.referenceImages.get 发送另一个 API 请求,以获取此参考图片的网址,以便将其显示在屏幕上。
  • product.labels:产品的预定义标记列表。如果您希望对搜索结果进行过滤,使其仅显示一种类别的服装(如连衣裙),这种做法非常有用。

将查询图片转换为 base64

您需要将查询图片转换为 base64 字符串表示形式,并将该字符串附加到请求正文中的 JSON 对象。

转到 ProductSearchAPIClient 类,找到空的 convertBitmapToBase64 方法并将其替换为此实现:

private fun convertBitmapToBase64(bitmap: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
    val byteArray: ByteArray = byteArrayOutputStream.toByteArray()
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
}

实现 API 调用

接下来,编写产品搜索 API 请求并将其发送到后端。您将使用 Volley 发出 API 请求,并使用 Task API 返回结果。

返回 ProductSearchAPIClient 类,找到空的 annotateImage 方法并将其替换为此实现:

fun annotateImage(image: Bitmap): Task<List<ProductSearchResult>> {
    // Initialization to use the Task API
    val apiSource = TaskCompletionSource<List<ProductSearchResult>>()
    val apiTask = apiSource.task

    // Convert the query image to its Base64 representation to call the Product Search API.
    val base64: String = convertBitmapToBase64(image)

    // Craft the request body JSON.
    val requestJson = """
        {
          "requests": [
            {
              "image": {
                "content": """".trimIndent() + base64 + """"
              },
              "features": [
                {
                  "type": "PRODUCT_SEARCH",
                  "maxResults": $VISION_API_PRODUCT_MAX_RESULT
                }
              ],
              "imageContext": {
                "productSearchParams": {
                  "productSet": "projects/${VISION_API_PROJECT_ID}/locations/${VISION_API_LOCATION_ID}/productSets/${VISION_API_PRODUCT_SET_ID}",
                  "productCategories": [
                       "apparel-v2"
                     ]
                }
              }
            }
          ]
        }
    """.trimIndent()

    // Add a new request to the queue
    requestQueue.add(object :
        JsonObjectRequest(
            Method.POST,
            "$VISION_API_URL/images:annotate?key=$VISION_API_KEY",
            JSONObject(requestJson),
            { response ->
                // Parse the API JSON response to a list of ProductSearchResult object/
                val productList = apiResponseToObject(response)

                // Return the list.
                apiSource.setResult(productList)
            },
            // Return the error
            { error -> apiSource.setException(error) }
        ) {
        override fun getBodyContentType() = "application/json"
    }.apply {
        setShouldCache(false)
    })

    return apiTask
}

在界面中显示搜索结果

现在,ProductSearchAPIClient 中的 API 代码已准备就绪。返回 activity ProductSearchActivity 实现界面代码。

该 activity 已包含一些可触发 searchByImage(queryImage: Bitmap) 方法的样板代码。添加代码以调用后端,并在界面中显示当前为空的方法的结果。

apiClient.annotateImage(queryImage)
    .addOnSuccessListener { showSearchResult(it) }
    .addOnFailureListener { error ->
        Log.e(TAG, "Error calling Vision API Product Search.", error)
        showErrorResponse(error.localizedMessage)
    }

showSearchResult 方法包含一些样板代码,用于解析 API 响应并将其显示在屏幕上。

开始运行

现在,点击 Android Studio 工具栏中的 Run 图标 (execute.png)。应用加载后,点按任何预设图片,选择检测到的对象,然后点按搜索按钮,即可查看从后端返回的搜索结果。您会看到类似如下的内容:

bb5e7c27c283a2fe.png

产品搜索结果屏幕的屏幕截图

后端已根据预设产品目录返回外观相似的产品列表。但是,您可以看到产品图片仍然是空的。这是因为 projects.locations.images.annotate 端点仅返回产品图片 ID(如 projects/odml-codelabs/locations/us-east1/products/product_id77/referenceImages/image77)。您需要对 projects.locations.products.referenceImages.get 端点再次进行 API 调用,获取此参考图片的网址,以便将其显示在屏幕上。

8. 获取产品参考图片

探索 API 请求和响应格式

您将向 projects.locations.products.referenceImages.get 端点发送带有空请求正文的 GET HTTP 请求,以获取产品搜索端点返回的产品图片的 URI。

HTTP 请求如下所示:

GET $VISION_API_URL/projects/odml-codelabs/locations/us-east1/products/product_id77/referenceImages/image77?key=$VISION_API_KEY

如果请求成功,服务器将返回一个 200 OK HTTP 状态代码以及 JSON 格式的响应,如下所示:

{
  "name":"projects/odml-codelabs/locations/us-east1/products/product_id77/referenceImages/image77",
  "uri":"gs://cloud-ai-vision-data/product-search-tutorial/images/46991e7370ba11e8a1bbd20059124800.jpg"
}
  • name:参考图片标识符
  • uri:Google Cloud Storage (GCS) 上图片的 URI。

演示产品搜索后端的参考图片设置为具有公开读取权限。因此,您可以轻松地将 GCS URI 转换为 HTTP 网址,并在应用界面中显示该网址。您只需将 gs:// 前缀替换为 https://storage.googleapis.com/ 即可。

实现 API 调用

接下来,编写产品搜索 API 请求并将其发送到后端。您将使用 Volley 和 Task API,类似于 Product Search API 调用。

返回 ProductSearchAPIClient 类,找到空的 fetchReferenceImage 方法并将其替换为此实现:

private fun fetchReferenceImage(searchResult: ProductSearchResult): Task<ProductSearchResult> {
    // Initialization to use the Task API
    val apiSource = TaskCompletionSource<ProductSearchResult>()
    val apiTask = apiSource.task

    // Craft the API request to get details about the reference image of the product
    val stringRequest = object : StringRequest(
        Method.GET,
        "$VISION_API_URL/${searchResult.imageId}?key=$VISION_API_KEY",
        { response ->
            val responseJson = JSONObject(response)
            val gcsUri = responseJson.getString("uri")

            // Convert the GCS URL to its HTTPS representation
            val httpUri = gcsUri.replace("gs://", "https://storage.googleapis.com/")

            // Save the HTTPS URL to the search result object
            searchResult.imageUri = httpUri

            // Invoke the listener to continue with processing the API response (eg. show on UI)
            apiSource.setResult(searchResult)
        },
        { error -> apiSource.setException(error) }
    ) {

        override fun getBodyContentType(): String {
            return "application/json; charset=utf-8"
        }
    }
    Log.d(ProductSearchActivity.TAG, "Sending API request.")

    // Add the request to the RequestQueue.
    requestQueue.add(stringRequest)

    return apiTask
}

此方法采用产品搜索端点返回的 searchResult: ProductSearchResult 对象,然后按照以下步骤操作:

  1. 调用参考图片端点以获取参考图片的 GCS URI。
  2. 将 GCS URI 转换为 HTTP 网址。
  3. 使用此 HTTP 网址更新 searchResult 对象的 httpUri 属性。

连接两个 API 请求

返回 annotateImage 并修改该网址,以获取所有参考图片的 HTTP 网址,然后再将 ProductSearchResult 列表返回给调用方。

找到以下行:

// Return the list.
apiSource.setResult(productList)

然后将其替换为以下实现:

// Loop through the product list and create tasks to load reference images.
// We will call the projects.locations.products.referenceImages.get endpoint
// for each product.
val fetchReferenceImageTasks = productList.map { fetchReferenceImage(it) }

// When all reference image fetches have completed,
// return the ProductSearchResult list
Tasks.whenAllComplete(fetchReferenceImageTasks)
    // Return the list of ProductSearchResult with product images' HTTP URLs.
    .addOnSuccessListener { apiSource.setResult(productList) }
    // An error occurred so returns it to the caller.
    .addOnFailureListener { apiSource.setException(it) }

屏幕上的显示参考图片的样板代码已为您在 ProductSearchAdapter 类中实现,因此您可以继续重新运行该应用。

开始运行

现在,点击 Android Studio 工具栏中的 Run 图标 (execute.png)。应用加载后,点按任意预设图片,选择检测到的对象,然后点按搜索按钮即可查看搜索结果,而这次使用的是产品图片。

产品搜索结果对您是否有意义?

25939f5a13eeb3c3.png

9. 恭喜!

您了解了如何调用 Vision API Product Search 后端,以便向 Android 应用添加产品图片搜索功能。完成设置并正常运行即可。

您后续可能需要使用产品目录构建您自己的后端。请查看产品图片搜索学习衔接课程中的下一个 Codelab,了解如何构建您自己的后端并设置 API 密钥,以便从移动应用调用该密钥。

所学内容

  • 如何从 Android 应用调用 Vision API Product Search 后端

后续步骤

了解详情