serde_jsonとreqwestを使ってGoogle Cloud Vision APIを使ってみた。

Rustでは[serde_json](https://docs.serde.rs/serde_json/)と[reqwest](https://docs.rs/reqwest/0.10.0-alpha.1/reqwest/)を使えば、お手軽にGoogle Cloud Vision(以下GCV)のAPIを利用できたので、今回はそれを紹介したいと思います。最終的な成果物はGistにて公開しています。[^1]今回の手法を利用すれば、他のサービスが提供しているAPIも利用可能だと思います。

[^1]: [Rust Playground](https://play.rust-lang.org/)にてコードを掲載しようと思ったのですが、利用可能な`hex`crateのバージョンが古すぎてビルドエラーとなったので、Gistにしました。 

https://gist.github.com/HirotoShioi/2db3354ac4e0951945161af4aaa225dd

### GCV APIを利用可能な状態にする

GCVを利用するには４つのステップを踏む必要があります。
1. GCPのアカウント登録
2. プロジェクトの新規作成
3. APIキーの取得
4. クレジットカードの登録
それぞれの手順については[公式ドキュメント](https://cloud.google.com/vision/docs/before-you-begin?hl=ja)を参考にしてください。

### jsonマクロを用いてRequestBodyを定義する

`serde_json`が提供している[json](https://docs.serde.rs/serde_json/macro.json.html)マクロを利用すれば[Cloud Vision公式ドキュメント](https://cloud.google.com/vision/docs/request?hl=ja)にて定義されているリクエストBodyがお手軽に生成可能です。

```rust
use serde_json::json;

let uri = "uri to image";
let request = json!({
    "requests": [
      {
        "image": {
            "source": {
                "imageUri": uri //変数も利用可能
            }
        },
        "features": [
          {
            "maxResults": 10,
            "type": "IMAGE_PROPERTIES"
          }
        ]
      }
    ]
});
```

### Clientを用いてリクエストURLを生成する

次は`reqwest`が提供するstruct, [Client](https://docs.rs/reqwest/0.10.0-alpha.1/reqwest/struct.Client.html)を用いてリクエストURIを作成し、送信します。


```rust
use reqwest::Client;

//APIキーが書き込まれているファイルを読み込む
let secret_key = fs::read_to_string(API_KEY_FILE_PATH)?;

let mut response = reqwest::Client::new()
    .post(CLOUD_VISION_URI)
    .query(&[("key", secret_key)])
    .json(&request)
    .send()?;
```

### 例外

リクエストは必ずしも成功するわけではありません。リクエストが失敗する方法は3通りあります。
1. そもそもGCV APIが使える状態にない(接続エラー、APIキーが無効、クレジットカードを登録していない、etc).
2. 無効なリクエスト
3. 画像のURLが無効だった

1と2に関しては返ってくるレスポンスのステータスコードで確認できますが、3に関しては返ってきたjsonをパースする必要があります。よって、例外処理は以下のように行います。

```rust
//ステータスコードがOk以外なら例外を投げる
if response.status() != StatusCode::OK {
    return Err(Box::new(CloudVisionError::BadRequest));
}

//Bodyをパースし、errorオブジェクトの有無を確認する
let response_json: Value = response.json()?;
let err = &response_json["responses"][0]["error"];

if err.is_object() {
    return Err(Box::new(CloudVisionError::FailedToParseImage));
}
```

##　途中経過

ここまでのことをまとめると以下のようなコードになります。

```rust
extern crate reqwest;
extern crate serde_json;

use reqwest::{StatusCode, Url};
use serde_json::{json, Value};

const CLOUD_VISION_URI: &str = "https://vision.googleapis.com/v1/images:annotate";
const API_KEY_FILE_PATH: &str = "./secrets/vision_api";

fn use_cloud_vision_api(image_url: &Url) -> Result<Value, Box<dyn std::error::Error>> {
    let request = json!({
        "requests": [
          {
            "image": {
                "source": {
                    "imageUri": image_url.to_owned().into_string()
                }
            },
            "features": [
              {
                "maxResults": 10,
                "type": "IMAGE_PROPERTIES"
              }
            ]
          }
        ]
    });

    let secret_key = fs::read_to_string(API_KEY_FILE_PATH)?;

    let mut response = reqwest::Client::new()
        .post(CLOUD_VISION_URI)
        .query(&[("key", secret_key)])
        .json(&request)
        .send()?;

    if response.status() != StatusCode::OK {
        return Err(Box::new(CloudVisionError::BadRequest));
    }

    let response_json: Value = response.json()?;

    let err = &response_json["responses"][0]["error"];

    if err.is_object() {
        return Err(Box::new(CloudVisionError::FailedToParseImage));
    }

    Ok(response_json)
}

#[derive(Debug)]
enum CloudVisionError {
    BadRequest,
    UnableToParseColorData,
}

impl std::error::Error for CloudVisionError {}

impl fmt::Display for CloudVisionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
        let error_message = match self {
            CloudVisionError::BadRequest => "Bad request",
            CloudVisionError::FailedToParseImage => "Cloud vision api failed to parse image",
        };
        write!(f, "{}", error_message)
    }
}
```

##その後

`Value`のままではあまり使いみちがありません。なので`Value`が提供するAPIを用いて必要な部分まで掘り下げて、そのあとは任意のstructへ変換します。以下は`dominantColor`オブジェクト内のリストを`Color` structに変換する例です。

```rust
extern crate hex;

#[derive(Debug)]
pub struct Color {
    pub pixel_fraction: f32,
    pub score: f32,
    pub hex_color: String,
}

fn extract_colors(val: &Value) -> Result<Vec<Color>, CloudVisionError> {
    //Valueのまま掘り下げる
    let colors = &val["responses"][0]["imagePropertiesAnnotation"]["dominantColors"]["colors"];

    match colors.as_array() {
        Some(color_ary) => {
            let mut color_vec = Vec::new();

            for color_value in color_ary.iter() {
                if let Some(color) = to_color(color_value) {
                    color_vec.push(color);
                } else {
                    return Err(CloudVisionError::UnableToParseColorData);
                };
            }
            Ok(color_vec)
        }

        None => Err(CloudVisionError::UnableToParseData),
    }
}

//Valueから任意のstructへ変換する
///Construct `Color` struct with given `Value`
fn to_color(value: &Value) -> Option<Color> {
    let pixel_fraction = value.get("pixelFraction")?.as_f64()? as f32;
    let score = value.get("score")?.as_f64()? as f32;

    let color = &value.get("color")?;
    let red:u8 = color.get("red")?.to_owned().as_u64()? as u8;
    let green:u8 = color.get("green")?.to_owned().as_u64()? as u8;
    let blue:u8 = color.get("blue")?.to_owned().as_u64()? as u8;

    //Construct hex string color
    let mut hex_color = String::from("#");

    let hex = hex::encode(vec![red, green, blue]);

    hex_color.push_str(&hex);

    let color_struct = Color {
        pixel_fraction,
        score,
        hex_color
    };

    Some(color_struct)
}
```

##その他

今回はGCVの画像プロパティを検出するAPIを利用する例を紹介しました。もしGCVが提供する他の機能（顔、ロゴの検出）を行いたい場合には以下のURLを参考にし、リクエストBodyを修正することで利用できると思います。

- [リクエストBodyの基本的な構造](https://cloud.google.com/vision/docs/reference/rest/v1/AnnotateImageRequest?hl=ja)
- [Featureオブジェクトを用いて他の機能を利用する方法](https://cloud.google.com/vision/docs/reference/rest/v1/Feature?hl=ja)

また手元にある画像を用いてGCVを利用することも可能です。その場合には画像をbase64でエンコーディングし、リクエストBodyを修正します。

```rust
use serde_json::json;

let base64_encoded = "base64 encoded image";
let request = json!({
    "requests": [
      {
        "image": {
            "content": base64_encoded
        },
        "features": [
          {
            "maxResults": 10,
            "type": "IMAGE_PROPERTIES"
          }
        ]
      }
    ]
});
```