如需从应用中调用 Google Cloud API,您需要创建一个中间 REST API 来处理授权并保护 API 密钥等密钥值。然后,您需要在移动应用中编写代码,用于向此中间服务进行身份验证并与其通信。
您可以使用 Firebase Authentication 和 Firebase Functions 来创建此 REST API,这样您便有了一个连接到 Google Cloud API 的代管式无服务器网关来处理身份验证,而且您可以通过预构建的 SDK 从自己的移动应用中调用此网关。
本指南演示了如何使用此方法从应用中调用 Cloud Vision API。此方法将允许所有经过身份验证的用户通过您的 Cloud 项目访问 Cloud Vision 收费服务,因此请考虑这种身份验证机制是否满足您的使用场景,然后再继续操作。
准备工作
配置您的项目
- 将 Firebase 添加到您的 Android 项目(如果尚未添加)。
-
如果您尚未为项目启用基于 Cloud 的 API,请立即完成以下操作:
- 打开 Firebase 控制台的 Firebase ML API 页面。
-
如果您尚未将项目升级到 Blaze 定价方案,请点击升级以执行此操作。(只有在您的项目未采用 Blaze 方案时,系统才会提示您进行升级。)
只有 Blaze 级项目才能使用基于 Cloud 的 API。
- 如果尚未启用基于 Cloud 的 API,请点击启用基于 Cloud 的 API。
- 配置您现有的 Firebase API 密钥以禁止访问 Cloud Vision API:
- 打开 Cloud 控制台中的凭据页面。
- 对于列表中的每个 API 密钥,打开修改视图,然后在“密钥限制”部分中,向列表中添加除了 Cloud Vision API 之外的所有可用 API。
部署 Callable 函数
接下来,部署将用于衔接您的应用与 Cloud Vision API 的 Cloud Functions 函数。functions-samples
代码库包含一个您可以使用的示例。
默认情况下,此函数将仅允许通过身份验证的应用用户访问 Cloud Vision API。您可以根据不同的要求修改该函数。
如需部署函数,请执行以下操作:
- 克隆或下载 functions-samples 代码库并切换到
Node-1st-gen/vision-annotate-image
目录:git clone https://github.com/firebase/functions-samples
cd Node-1st-gen/vision-annotate-image
- 安装依赖项:
cd functions
npm install
cd ..
- 如果您没有 Firebase CLI,请进行安装。
- 在
vision-annotate-image
目录中初始化 Firebase 项目。收到提示时,请从列表中选择您的项目。firebase init
- 部署函数:
firebase deploy --only functions:annotateImage
将 Firebase Auth 添加到您的应用
上面部署的 Callable 函数将拒绝未经身份验证的应用用户的任何请求。如果您尚未将 Firebase Auth 添加到您的应用,则需要执行此操作。
为应用添加必要的依赖项
implementation("com.google.firebase:firebase-functions:20.4.0") implementation("com.google.code.gson:gson:2.8.6")
1. 准备输入图片
为了调用 Cloud Vision,图片的格式必须为 base64 编码字符串。如需处理保存的文件 URI 中的图片,请执行以下操作:- 以
Bitmap
对象的形式获取图片:Kotlin+KTX
var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
Java
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
- (可选)缩小图片以节省带宽。请参阅 Cloud Vision 推荐的图片大小。
Kotlin+KTX
private fun scaleBitmapDown(bitmap: Bitmap, maxDimension: Int): Bitmap { val originalWidth = bitmap.width val originalHeight = bitmap.height var resizedWidth = maxDimension var resizedHeight = maxDimension if (originalHeight > originalWidth) { resizedHeight = maxDimension resizedWidth = (resizedHeight * originalWidth.toFloat() / originalHeight.toFloat()).toInt() } else if (originalWidth > originalHeight) { resizedWidth = maxDimension resizedHeight = (resizedWidth * originalHeight.toFloat() / originalWidth.toFloat()).toInt() } else if (originalHeight == originalWidth) { resizedHeight = maxDimension resizedWidth = maxDimension } return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false) }
Java
private Bitmap scaleBitmapDown(Bitmap bitmap, int maxDimension) { int originalWidth = bitmap.getWidth(); int originalHeight = bitmap.getHeight(); int resizedWidth = maxDimension; int resizedHeight = maxDimension; if (originalHeight > originalWidth) { resizedHeight = maxDimension; resizedWidth = (int) (resizedHeight * (float) originalWidth / (float) originalHeight); } else if (originalWidth > originalHeight) { resizedWidth = maxDimension; resizedHeight = (int) (resizedWidth * (float) originalHeight / (float) originalWidth); } else if (originalHeight == originalWidth) { resizedHeight = maxDimension; resizedWidth = maxDimension; } return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false); }
Kotlin+KTX
// Scale down bitmap size bitmap = scaleBitmapDown(bitmap, 640)
Java
// Scale down bitmap size bitmap = scaleBitmapDown(bitmap, 640);
- 将位图对象转换为 base64 编码的字符串:
Kotlin+KTX
// Convert bitmap to base64 encoded string val byteArrayOutputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) val imageBytes: ByteArray = byteArrayOutputStream.toByteArray() val base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP)
Java
// Convert bitmap to base64 encoded string ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream); byte[] imageBytes = byteArrayOutputStream.toByteArray(); String base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
以
Bitmap
对象表示的图片必须保持竖直,不需要进行额外的旋转。
2. 调用 Callable 函数来识别地标
如需识别图片中的地标,调用 Callable 函数,并向其传递 JSON Cloud Vision 请求。首先,初始化 Cloud Functions 的一个实例:
Kotlin+KTX
private lateinit var functions: FirebaseFunctions // ... functions = Firebase.functions
Java
private FirebaseFunctions mFunctions; // ... mFunctions = FirebaseFunctions.getInstance();
定义调用函数的方法:
Kotlin+KTX
private fun annotateImage(requestJson: String): Task<JsonElement> { return functions .getHttpsCallable("annotateImage") .call(requestJson) .continueWith { task -> // This continuation runs on either success or failure, but if the task // has failed then result will throw an Exception which will be // propagated down. val result = task.result?.data JsonParser.parseString(Gson().toJson(result)) } }
Java
private Task<JsonElement> annotateImage(String requestJson) { return mFunctions .getHttpsCallable("annotateImage") .call(requestJson) .continueWith(new Continuation<HttpsCallableResult, JsonElement>() { @Override public JsonElement then(@NonNull Task<HttpsCallableResult> task) { // This continuation runs on either success or failure, but if the task // has failed then getResult() will throw an Exception which will be // propagated down. return JsonParser.parseString(new Gson().toJson(task.getResult().getData())); } }); }
创建 JSON 请求,并将 Type 设置为
LANDMARK_DETECTION
:Kotlin+KTX
// Create json request to cloud vision val request = JsonObject() // Add image to request val image = JsonObject() image.add("content", JsonPrimitive(base64encoded)) request.add("image", image) // Add features to the request val feature = JsonObject() feature.add("maxResults", JsonPrimitive(5)) feature.add("type", JsonPrimitive("LANDMARK_DETECTION")) val features = JsonArray() features.add(feature) request.add("features", features)
Java
// Create json request to cloud vision JsonObject request = new JsonObject(); // Add image to request JsonObject image = new JsonObject(); image.add("content", new JsonPrimitive(base64encoded)); request.add("image", image); //Add features to the request JsonObject feature = new JsonObject(); feature.add("maxResults", new JsonPrimitive(5)); feature.add("type", new JsonPrimitive("LANDMARK_DETECTION")); JsonArray features = new JsonArray(); features.add(feature); request.add("features", features);
最后,调用函数:
Kotlin+KTX
annotateImage(request.toString()) .addOnCompleteListener { task -> if (!task.isSuccessful) { // Task failed with an exception // ... } else { // Task completed successfully // ... } }
Java
annotateImage(request.toString()) .addOnCompleteListener(new OnCompleteListener<JsonElement>() { @Override public void onComplete(@NonNull Task<JsonElement> task) { if (!task.isSuccessful()) { // Task failed with an exception // ... } else { // Task completed successfully // ... } } });
3. 获取识别出的地标的相关信息
如果地标识别操作成功,任务结果中将返回一个 BatchAnnotateImagesResponse JSON 响应。landmarkAnnotations
数组中的每个对象表示图片中识别出的一个地标。对于每个地标,您可以获取它在输入图片中的边界坐标、地标名称、地标的经纬度、地标的知识图谱实体 ID(如果有)以及匹配的置信度分数。例如:
Kotlin+KTX
for (label in task.result!!.asJsonArray[0].asJsonObject["landmarkAnnotations"].asJsonArray) {
val labelObj = label.asJsonObject
val landmarkName = labelObj["description"]
val entityId = labelObj["mid"]
val score = labelObj["score"]
val bounds = labelObj["boundingPoly"]
// Multiple locations are possible, e.g., the location of the depicted
// landmark and the location the picture was taken.
for (loc in labelObj["locations"].asJsonArray) {
val latitude = loc.asJsonObject["latLng"].asJsonObject["latitude"]
val longitude = loc.asJsonObject["latLng"].asJsonObject["longitude"]
}
}
Java
for (JsonElement label : task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("landmarkAnnotations").getAsJsonArray()) {
JsonObject labelObj = label.getAsJsonObject();
String landmarkName = labelObj.get("description").getAsString();
String entityId = labelObj.get("mid").getAsString();
float score = labelObj.get("score").getAsFloat();
JsonObject bounds = labelObj.get("boundingPoly").getAsJsonObject();
// Multiple locations are possible, e.g., the location of the depicted
// landmark and the location the picture was taken.
for (JsonElement loc : labelObj.get("locations").getAsJsonArray()) {
JsonObject latLng = loc.getAsJsonObject().get("latLng").getAsJsonObject();
double latitude = latLng.get("latitude").getAsDouble();
double longitude = latLng.get("longitude").getAsDouble();
}
}