ScalaでCloud Vision API Clientを叩く

約3500枚の画像からラベルを抽出して機械学習の特徴量にしたいとします。
これを手でラベル付けするのは大変なので、ScalaでGoogle Cloud Vision API Clientを叩いて、画像のラベルを抽出します。
まずは、手始めにJavaのガイドをScalaに書き換えてみました。

# そもそも、なんでScala？
初めはCloud Vision API Clientを使ったスクリプトをPythonで書いていました。
しかし、そのスクリプトはある一定数の画像を処理すると、APIが使っているurllib3に起因するエラーでコケてしまいました。
業務で毎日使っているPythonに少し飽きてきて、込み入ったエラーを修正しようという気がなかなか湧いてきません。
ちょうどその時、Scala + Apache Sparkの組み合わせがPython + Pandasの代替としてローカル環境で使えないかなと考えていたで、Scalaに慣れる意味も込めてJavaコードの書換えに挑戦しました。

# 書き換えるJavaコード
書き換えるコードは次のURLに掲載されている以下のコードです。
[https://cloud.google.com/vision/docs/libraries#client-libraries-install-java](https://cloud.google.com/vision/docs/libraries#client-libraries-install-java)

```java
// Imports the Google Cloud client library

import com.google.cloud.vision.v1.AnnotateImageRequest;
import com.google.cloud.vision.v1.AnnotateImageResponse;
import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
import com.google.cloud.vision.v1.EntityAnnotation;
import com.google.cloud.vision.v1.Feature;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.cloud.vision.v1.Image;
import com.google.cloud.vision.v1.ImageAnnotatorClient;
import com.google.protobuf.ByteString;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class QuickstartSample {
  public static void main(String... args) throws Exception {
    // Instantiates a client
    try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {

      // The path to the image file to annotate
      String fileName = "./resources/wakeupcat.jpg";

      // Reads the image file into memory
      Path path = Paths.get(fileName);
      byte[] data = Files.readAllBytes(path);
      ByteString imgBytes = ByteString.copyFrom(data);

      // Builds the image annotation request
      List<AnnotateImageRequest> requests = new ArrayList<>();
      Image img = Image.newBuilder().setContent(imgBytes).build();
      Feature feat = Feature.newBuilder().setType(Type.LABEL_DETECTION).build();
      AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
          .addFeatures(feat)
          .setImage(img)
          .build();
      requests.add(request);

      // Performs label detection on the image file
      BatchAnnotateImagesResponse response = vision.batchAnnotateImages(requests);
      List<AnnotateImageResponse> responses = response.getResponsesList();

      for (AnnotateImageResponse res : responses) {
        if (res.hasError()) {
          System.out.printf("Error: %s\n", res.getError().getMessage());
          return;
        }

        for (EntityAnnotation annotation : res.getLabelAnnotationsList()) {
          annotation.getAllFields().forEach((k, v) ->
              System.out.printf("%s : %s\n", k, v.toString()));
        }
      }
    }
  }
}
```

# 準備
今回はsbtを使います。
build.sbtは上で紹介したAPIドキュメントに記載されている通りの設定です。

```build.sbt
libraryDependencies ++= Seq(
  "com.google.cloud" % "google-cloud-vision" % "1.27.0"
)
```

テストで使う画像は「くまモン」です。
![00000001.jpg](https://qiita-image-store.s3.amazonaws.com/0/218401/f6e1fa7c-9a51-1b76-bbf0-cf18fbb26592.jpeg)

# Scalaで書き換えたコード

```scala:Main.scala
import com.google.cloud.vision.v1.AnnotateImageRequest
import com.google.cloud.vision.v1.Image
import com.google.cloud.vision.v1.ImageAnnotatorClient
import com.google.cloud.vision.v1.Feature
import com.google.cloud.vision.v1.Feature.Type
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import scala.collection.JavaConverters._

object QuickstartSample {
  def main(args: Array[String]): Unit = {

      val filePath = Paths.get("/path/to/your/image.jpg")

      // 画像をbase64に変換する
      val data       = Files.readAllBytes(filePath)
      val imageBytes = ByteString.copyFrom(data)
      val image      = Image.newBuilder().setContent(imageBytes).build()

      // Cloud Vision APIへのリクエストを準備する
      val feature = Feature.newBuilder().setType(Type.LABEL_DETECTION).build()
      val request = AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(image).build()
      val requests = List(request)  // 今回は1つの画像だけなので、Listに要素を追加する形にはしない

      // 画像のラベルを取得する
      val vision = ImageAnnotatorClient.create()
      // Java APIで使えるようにするためrequestsを変換する
      val response  = vision.batchAnnotateImages(requests.asJava)
      val responses = response.getResponsesList()
      responses.iterator().asScala.foreach{ res =>
        // getLabelAnnotationsListで返ってくるのはJavaのMapオブジェクトなので、
        // .iterator().asScalaでScalaのMapに変換する
        val annotationsList = res.getLabelAnnotationsList().iterator().asScala
        annotationsList.foreach { annotation =>
          println("---------------------------")
          annotation.getAllFields().asScala.foreach{case (k, v) => println(s"$k: $v")}
        }
      }

  }

}
```

# 実行結果
まずはsbtを起動します。

```shell-session
(tensorflow) ishiyama@tensorflow:~/yuruchara/TestLableDetection$ sbt
[info] Loading project definition from /home/ishiyama/yuruchara/TestLableDetection/project
[info] Loading settings from build.sbt ...
[info] Set current project to testlabledetection (in build file:/home/ishiyama/yuruchara/TestLableDetection/)
[info] sbt server started at local:///home/ishiyama/.sbt/1.0/server/a7cbba64b3080062510f/sock
```

次にコンパイルします。
初回の場合はライブラリをDLするので時間が掛かります。
2回目以降は割とすぐコンパイルできてしまいます。

```shell-session
sbt:testlabledetection> compile
[info] Compiling 1 Scala source to /home/ishiyama/yuruchara/TestLableDetection/target/scala-2.12/classes ...
[info] Done compiling.
[success] Total time: 2 s, completed May 30, 2018 2:42:14 AM
```

最後にrunで実行します。

```shell-session
sbt:testlabledetection> run
[info] Packaging /home/ishiyama/yuruchara/TestLableDetection/target/scala-2.12/testlabledetection_2.12-0.1.0-SNAPSHOT.jar ...
[info] Done packaging.
[info] Running QuickstartSample 
---------------------------
google.cloud.vision.v1.EntityAnnotation.mid: /m/021mh_
google.cloud.vision.v1.EntityAnnotation.description: stuffed toy
google.cloud.vision.v1.EntityAnnotation.score: 0.87299186
google.cloud.vision.v1.EntityAnnotation.topicality: 0.87299186
---------------------------
google.cloud.vision.v1.EntityAnnotation.mid: /m/0153bq
google.cloud.vision.v1.EntityAnnotation.description: mascot
google.cloud.vision.v1.EntityAnnotation.score: 0.8525236
google.cloud.vision.v1.EntityAnnotation.topicality: 0.8525236
---------------------------
google.cloud.vision.v1.EntityAnnotation.mid: /m/025rgl
google.cloud.vision.v1.EntityAnnotation.description: plush
google.cloud.vision.v1.EntityAnnotation.score: 0.7825093
google.cloud.vision.v1.EntityAnnotation.topicality: 0.7825093
[success] Total time: 3 s, completed May 30, 2018 2:42:20 AM
```

descriptionがラベルでscoreが確率になっています。
あまりパッとしたラベルが取れていないですね。
この手の画像のラベル抽出は不得手なのかもしれません。

# まとめ
今回はGoogleのAPIドキュメントに書かれているJavaコードをScalaに書き換えて、ラベル抽出を実行しました。
インポートするパッケージが多いので行数も多くなっていますが、実際の処理部分のコード量は少なくなっていますね。
Pythonに慣れてしまっている私にとっては、Java APIを使う部分でJavaのデータ型からScalaのデータ型に変換する部分で戸惑いましたが、Javaエコシステムの恩恵を受けられるので、慣れれば幅が広がるなと感じました。
HadoopやScalaのようにデータ分析系のツールを使えることも私とっては良い印象です。
一方で、言語自体の盛り上がりに欠ける印象があり、今後の動向が気になるところです。
また、Androidの公式開発言語に採用されたKotlinでも同じことができるはずなので、データ分析系のツールがこちらで開発される事例も出てくるのかなと気になります。
いずれにしても、まずはScalaに慣れるためにこんな感じの記事を投稿していきたいです。

# 参考URL
 - https://cloud.google.com/vision/docs/libraries#client-libraries-usage-java
 - https://stackoverflow.com/questions/38339440/how-to-convert-a-java-stream-to-a-scala-stream