LoginSignup
6
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-10

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

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

GCVを利用するには4つのステップを踏む必要があります。
1. GCPのアカウント登録
2. プロジェクトの新規作成
3. APIキーの取得
4. クレジットカードの登録
それぞれの手順については公式ドキュメントを参考にしてください。

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

serde_jsonが提供しているjsonマクロを利用すればCloud Vision公式ドキュメントにて定義されているリクエストBodyがお手軽に生成可能です。

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を用いてリクエストURIを作成し、送信します。

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をパースする必要があります。よって、例外処理は以下のように行います。

//ステータスコードが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));
}

 途中経過

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

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に変換する例です。

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を修正することで利用できると思います。

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

use serde_json::json;

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

  1. Rust Playgroundにてコードを掲載しようと思ったのですが、利用可能なhexcrateのバージョンが古すぎてビルドエラーとなったので、Gistにしました。  

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5