在 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 等),然后选择之前下载的源代码中的 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)) 这是一个回调,用于接收仅包含用户点按的对象的剪裁图片。系统会将此剪裁后的图片发送到商品搜索后端。

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

转到 ObjectDetectorActivity 类中的 initViews 方法,并在该方法末尾添加以下行:(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

现在,您将在单独的 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 以执行商品搜索序列。
  • 在启动 activity intent 中添加剪裁后的图像 URI,以便 ProductSearchActivity 稍后可以检索它以用作查询图片。

请注意以下几点:

  • 用于检测对象和查询后端的逻辑已拆分为 2 个 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 79a5fc6c829eca77.png 3528c872f813826e.png

预设产品目录中的示例

调用商品搜索演示后端

您可以设置 Google Cloud API 密钥并将 API 密钥限制为仅供您的应用访问,从而直接通过移动应用调用 Vision API Product Search。

为简单起见,此 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):将 Bitmap 实例转换为其 base64 表示法以发送到商品搜索后端
  • fun annotateImage(image: Bitmap): Task<List<ProductSearchResult>>:调用 projects.locations.images.annotation 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_网址 是 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 是您要在其中搜索外观相似的商品的产品目录的 ID(在 Vision API 中也称为“商品集”)。一个 Cloud 项目中可以有多个目录。product_set0 是演示后端的预设商品清单。

7. 调用 Product Search API

探索 API 请求和响应格式

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

您需要使用此 JSON 正文向 projects.locations.images.annotation 端点发送 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.annotation 端点只会返回 projects/odml-codelabs/locations/us-east1/products/product_id77/referenceImages/image77 等产品图片 ID。您需要对 projects.locations.products.referenceImages.get 端点再进行一次 API 调用,并获取此参考图片的网址,以便在屏幕上显示此图片。

8. 获取商品参考图片

探索 API 请求和响应格式

您需要将包含空白请求正文的 GET HTTP 请求发送到 projects.locations.products.referenceImages.get 端点,以获取商品搜索端点返回的商品图片的 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 后端

后续步骤

了解详情