[Vision API Product Search] Cloud Vision APIをJavaで使ってみる(画像検索)

Googleが提供しているVision API Product Searchを
Javaで使ってみました。
Googleサービスアカウントを使用して、登録した商品画像に対して
ローカルにある画像から類似画像の検索をかけてみました。

***Vision API Product Searchとは***
Googleが提供しているAPIで、登録した画像から類似画像を検索するサービスです。

料金は、月間費用で予測費用とストレージ費用に分かれていて、
登録画像数が1000枚までならば、
　どちらも無料。
登録画像数が500万枚までならば、
　予測費用が＄4.5/1000枚、ストレージ費用が＄0.1/1000枚
登録画像数が2000万枚までならば、
　予測費用が＄1.8/1000枚、ストレージ費用が＄0.1/1000枚
となっています。

|No||目次|
|---|---|---|
|1||[Cloud Vision APIを使用するための準備](#1-cloud-vision-apiを使用するための準備)|
||1|[プロジェクトの作成](#11-プロジェクトの作成)|
||2|[Cloud Vision APIの設定](#12-cloud-vision-apiの設定)|
||3|[Googleサービスアカウントの作成](#13-googleサービスアカウントの作成)|
|2||[Cloud Vision APIを使ってみる](#2-cloud-vision-apiを使ってみる)|
||1|[googleライブラリの読み込み](#21-googleライブラリの読み込み)|
||2|[商品情報の登録](#23-商品情報の登録)|
||3|[商品画像で検索](#24-商品画像で検索)|

# 1. Cloud Vision APIを使用するための準備
設定は下記から行います。
[⧉Google クラウド プラットフォーム](https://console.cloud.google.com/)

### 1.1. プロジェクトの作成
こちらの記事を参照ください
[⧉[Google Sheets API] Google Sheets API v4をJavaで操作する(1.1. プロジェクトの作成)](https://qiita.com/doran/items/3d43f776188b2281044c/#11-プロジェクトの作成)

### 1.2. Cloud Vision APIの設定

Cloud Vision APIを使えるようにします。
###### 1. メニューの 「APIとサービス」 -> 「ライブラリ」 を選択します。
<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/2fda7515-ffdf-11d7-4142-fed896cc8413.png">

###### 2. Croud Visionと入力して「Cloud Vision API」を検索します。
<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/7eb491f1-af1d-d05d-4f2c-ae3c435bc996.png">

###### 3. 「有効にする」ボタンを押して「Cloud Vision API」を使用可能にします。
<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/ceb33b84-c310-6499-5bcd-e36a72d70418.png">

### 1.3. Googleサービスアカウントの作成
こちらの記事を参照ください
[⧉[Google Sheets API] Google Sheets API v4をJavaで操作する(1.3. Googleサービスアカウントの作成)](https://qiita.com/doran/items/3d43f776188b2281044c/#13-googleサービスアカウントの作成)

上記の記事に加え、
**サービスアカウント作成時に「Storageオブジェクト閲覧者」の権限を付与します。**
(サービスアカウント作成後でもOK)
Cloud Vision APIで登録する商品画像は、Cloud Storageに登録されている画像のみが
参照可能となるためです。
（商品への登録画像は、ローカルの画像を参照させることはできません。）
<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/b2986c29-9b61-c553-2656-a20843f859ba.png">
<img width="240" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/c78f6033-3789-68a8-6a09-09125a56c4ec.png">

### 1.4. Cloud Storageに使用する画像を置く
###### 1. Cloud Storageに商品に登録する画像をおきます。
<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/b0423151-cb1c-2d88-3f39-a46ad797a447.png">

###### 2. 登録した画像を選択して「gsutil URIコピー」を選択します。
このURLは後のプログラムで使用します。
<img width="240" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114621/da89886c-377e-5648-0842-4d263777bccd.png">


# 2. Cloud Vision APIを使ってみる
### 2.1. googleライブラリの読み込み
Cloud Vision APIを使用するためにライブラリのパスを設定します。
私の環境はpom.xmlで下記を指定しています。
Javaのバージョンは21を使用しています。

dependenciesと同じ階層にdependencyManagementを追加します。
```xml
<dependencies>
....
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-vision</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>libraries-bom</artifactId>
            <version>26.31.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
```
### 2.2. CredentialsProviderの取得
サービスアカウントのJSONを指定してCredentialsProviderを取得します。

ファイルパスには、[1.3.　Googleサービスアカウントの作成](#13-googleサービスアカウントの作成)
でダウンロードしたJSONファイルのパスを指定してください。
```java
private static CredentialsProvider getCredentialsProvider() throws IOException {
    String jsonFilePath = "サービスアカウントJSONキーファイルのパス";
    FileInputStream jsonFileInputStream = new FileInputStream(jsonFilePath);
    GoogleCredentials credentials = GoogleCredentials.fromStream(jsonFileInputStream);

    return FixedCredentialsProvider.create(credentials);
}
```
### 2.3. 商品情報の登録
商品情報を登録します。

**登録の順番**
商品セットをまず作成し、それに対して商品を追加していきます。

1.**商品セット**の作成
2.**商品**の作成
3.商品セット(上記1.)に対して商品(上記2.)を登録
4.商品(上記2.)に対して**画像**を登録

**それぞれの情報**
商品セット、商品、画像のそれぞれの情報に下記を設定しています。

商品セットの情報には、
・商品セットID、商品セット名
商品の情報には、
・商品ID、商品名、商品のカテゴリ、商品画像
商品の画像には、
・画像ID、画像URL(Cloud Storageの画像パス)

**画像の推奨事項**
登録する商品画像はgoogleによると下記が推奨されているそうです。

・ファイルサイズは最大20MB
・商品の特徴となる部分をできるだけ画像内に収める
・複数をセットとする場合は全て画像に入れる（靴なら、右足だけではなく、左足も入れる）
・背景は白
・解像度を高くする
・PNGで透過ありなら、背景が透明なら単色の色する

```java
public static void main(String[] args) throws IOException{
    String productSetId = "トミカ";                //商品セットのID
    String productSetDisplayName = "トミカシリーズ"; //商品セット名
    String productCategory = "toys-v2";         //商品のカテゴリ
    String[][] product_list = getProductList(); //登録する商品情報
    
    String locationName = LocationName.format(PROJECT_ID, COMPUTE_REGION);
    try (ProductSearchClient client = getProductSearchClient()) {
        //1.商品セットを作成
        CreateProductSetRequest request = CreateProductSetRequest.newBuilder()
                .setParent(locationName)
                .setProductSetId(productSetId) //商品セットID
                .setProductSet(ProductSet.newBuilder().setDisplayName(productSetDisplayName).build())
                                               //商品セット名
                .build();
        ProductSet productSet = client.createProductSet(request);

        for (String[] p : product_list) {
            //2.商品を作成
            Product myProduct = Product.newBuilder()
                    .setName(p[0])        //商品ID
                    .setDisplayName(p[1]) //商品名
                    .setProductCategory(productCategory) //商品カテゴリ
                    .build();
            Product product = client.createProduct(
                    locationName,
                    myProduct,           //商品情報
                    p[0]);               //商品ID

            //3.商品セットに商品を追加
            client.addProductToProductSet(productSet.getName(), product.getName());

            //4.商品画像を登録
            client.createReferenceImage(
                    product.getName(), //商品リソース名
                    ReferenceImage.newBuilder().setUri(p[2]).build(), //商品画像
                    p[0]);             //画像ID(1枚だけなのでとりあえず商品IDと一緒)
        }
    }
}
```
ProductSearchClientインスタンスを生成します。
商品関連の情報を扱うメソッドが提供されます。
```java
private static ProductSearchClient getProductSearchClient() throws IOException {
    ProductSearchSettings productSearchSettings = ProductSearchSettings.newBuilder()
            .setCredentialsProvider(getCredentialsProvider()).build();
    return ProductSearchClient.create(productSearchSettings);
}
```
登録する商品のリストです。
本来クラスを用意してまとめた方がいいかもしれませんが、
とりあえずなので配列で用意しています。
（自前で撮影したトミカの画像を使用しています）
商品ID、商品名、商品画像(Cloud Storageのパス)の順です。
```java
private static String[][] getProductList(){
    return { //商品ID、商品名、商品画像(Cloud Storageのパス)
        { "トミカ No.48",  "日野 プロフィア 葛飾トラック","gs://test_doran/IMG_7830.jpg" },
        { "トミカ No.100", "スズキ ジムニー JAF ロードサービスカー","gs://test_doran/IMG_7831.jpg" },
        { "トミカ No.101", "いすゞ ギガ ダンプカー","gs://test_doran/IMG_7832.jpg" },
        { "トミカ No.45",  "トヨタ ダイナ 清掃車", "gs://test_doran/IMG_7833.jpg" },
        { "トミカ プレミアム No.16", "陸上自衛隊 16式機動戦闘車","gs://test_doran/IMG_7834.jpg" }
    };
}
```

### 2.4. 商品画像で検索
ローカル画像を使用して登録した商品画像の類似画像を検索します。
```java
public static void main(String[] args) throws IOException{
    String productSetId = "トミカ";         //検索する商品セットID
    String productCategory = "toys-v2";  //検索する商品のカテゴリ
    String path = "ローカルファイルのパス";    //検索する画像のパス
    try (ImageAnnotatorClient client = getImageAnnotatorClient()) {

        Feature featuresElement = Feature.newBuilder().setType(Type.PRODUCT_SEARCH).build();
        Image image = Image.newBuilder().setContent(ByteString.copyFrom(Files.readAllBytes(new File(path).toPath()))).build();
        ImageContext imageContext = ImageContext.newBuilder()
                .setProductSearchParams(
                        ProductSearchParams.newBuilder()
                                .setProductSet(getProductSetName(productSetId))
                                .addProductCategories(productCategory))
                .build();

        AnnotateImageRequest annotateImageRequest = AnnotateImageRequest.newBuilder()
                .addFeatures(featuresElement)
                .setImage(image)
                .setImageContext(imageContext)
                .build();
        
        BatchAnnotateImagesResponse response = client.batchAnnotateImages(Arrays.asList(annotateImageRequest));
        output(response);
    }
}
```
ImageAnnotatorClientインスタンスを生成します。
画像を扱うメソッドが提供されます。
```java
private static ImageAnnotatorClient getImageAnnotatorClient() throws IOException {
    ImageAnnotatorSettings imageAnnotatorSettings = ImageAnnotatorSettings.newBuilder()
            .setCredentialsProvider(getCredentialsProvider()).build();
    return ImageAnnotatorClient.create(imageAnnotatorSettings);
}
```
検索結果を出力しています。
実行結果のResultでは以下の情報が取得できます。
|名称|||概要|
|---|---|---|---|
|商品情報|Product|name|商品のリソース名|
|||displayName|商品名|
|||description|商品概要|
|||productCategory|商品カテゴリ|
|||productLabel|商品に添付しているキーと値のペア|
|スコア|score||0〜1までのfloat値。<br>数値が高いほど信頼度が高いです。|
|画像名|image||画像のリソース名|

```java
private static void output(BatchAnnotateImagesResponse response) {
    System.out.println(response);
    List<Result> similarProducts = response.getResponses(0).getProductSearchResults().getResultsList();
    for (Result product : similarProducts) {
        System.out.println("--------------");
        System.out.println(("商品リソース名: "+product.getProduct().getName()));
        System.out.println("商品名: "+product.getProduct().getDisplayName());
        System.out.println("スコア: "+product.getScore());
        System.out.println("画像名: "+product.getImage());
        System.out.println("--------------");
    }
}
```
**実行結果**
近しい画像が検索されました。
スコアの高い方がより近い画像を示しています。
ここらへんはまた別の記事で検証したいと思います。
```
--------------
商品リソース名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.48
商品名: 日野 プロフィア 葛飾トラック
スコア: 0.7208772
画像名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.48/referenceImages/トミカ No.48
--------------
--------------
商品リソース名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.45
商品名: トヨタ ダイナ 清掃車
スコア: 0.4311223
画像名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.45/referenceImages/トミカ No.45
--------------
--------------
商品リソース名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.101
商品名: いすゞ ギガ ダンプカー
スコア: 0.33746362
画像名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.101/referenceImages/トミカ No.101
--------------
--------------
商品リソース名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.100
商品名: スズキ ジムニー JAF ロードサービスカー
スコア: 0.29908606
画像名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ No.100/referenceImages/トミカ No.100
--------------
--------------
商品リソース名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ プレミアム No.16
商品名: 陸上自衛隊 16式機動戦闘車
スコア: 0.20026323
画像名: projects/astute-tractor-400101/locations/asia-east1/products/トミカ プレミアム No.16/referenceImages/トミカ プレミアム No.16
--------------
```
<br>
おしまい。。。