Google Cloud Vision APIとOpenAIを組み合わせて猫の画像を判別してみた

## 概要
猫の画像を読み込んで、色、柄、猫種を解析し、結果をmd形式で出力するプログラムを作成しました。

## 環境
- M2 Macbook
- Poetryで仮想環境を作成
- Python 3.12.3
- shellはzsh

## 流れ
OpenAIは無料期間が終わってクレジットを購入済みのため、Vision APIの有効化から行っていきます。
※GCPは初めて使ったのですが、無料で90日間つかえる$300分のクレジットがついたので、これからガンガン触っていきたいです。

### ■Vision APIの有効化

GCPでプロジェクトを作成し、「APIとサービス」 > 「ライブラリ」を選択しましょう。
![スクリーンショット 2024-06-17 16.58.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/efc54c3a-743e-9ad2-bebb-1c4f756bc04d.png)

検索バーに「Vision API」と入力し、Google Cloud Vision APIを選択します。
![スクリーンショット 2024-06-17 16.59.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/cd63706a-1405-d540-e322-ca3eb635ce3d.png)

「有効にする」ボタンをクリックしてAPIを有効にします。
![スクリーンショット 2024-06-17 16.58.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/d28cf5e9-77a7-84b1-ac9c-78c89d125665.png)

### ■サービスアカウントの作成とキーの取得
ナビゲーションメニューを開き、「IAMと管理」 > 「サービスアカウント」を選択: メニューから「IAMと管理」セクションの「サービスアカウント」を選択
![スクリーンショット 2024-06-17 17.01.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/2e84fc9a-da6f-ff61-60e0-6b04153ae1c7.png)

新しいサービスアカウントを作成: 上部の「+ サービスアカウントを作成」をクリックして名称などを入力。
![スクリーンショット 2024-06-17 17.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/e58cf10a-d53b-c6bd-dca4-542c05cc98c2.png)

ロールは「編集者」を選択 > 完了
![スクリーンショット 2024-06-18 9.31.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/de6f633a-1bfe-366b-b657-51317a1b55e8.png)

サービスアカウントができたら、鍵を作成します。「キータイプ」セクションで「JSON」を選択して作成。
![スクリーンショット 2024-06-18 9.33.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/55a42d74-dc5f-fca2-817a-a2984c0d5ad4.png)
鍵がダウンロードされるので、安全な場所に保存しておきます。

### ■環境変数の設定
下記を.zshrcに追加して環境変数を設定しておきます。
```shell
# Google Cloud Vision API
export GOOGLE_APPLICATION_CREDENTIALS="path/to/your/service-account-file.json"
# ""内は鍵を保存したパスに書き換えてください。
```
※OpenAIのOPENAI_API_KEYも(設定してない場合は)追加しておく。

### ■Poetryで仮想環境を作成

```shell
$ poetry init
$ poetry add openai google-cloud-vision 
```
フォルダ構成
```
.
├── analyze_cats.py
├── images
│   ├── nekocyan01.jpg
│   ├── ...
│   └── nekocyan10.jpg
├── output
├── poetry.lock
└── pyproject.toml
```
## ソースコード

```python:analyze_cats.py
import os
from google.cloud import vision
from openai import OpenAI

def analyze_image(image_path):
    client = vision.ImageAnnotatorClient()

    with open(image_path, 'rb') as image_file:
        content = image_file.read()

    image = vision.Image(content=content)
    response = client.label_detection(image=image)
    labels = response.label_annotations
    description = [label.description for label in labels]
    if response.error.message:
        raise Exception(f'{response.error.message}')

    return description

def get_cat_details(descriptions):
    client = OpenAI()
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "あなたは猫の品種、色、柄を識別する専門家です。すべての回答を日本語で返答してください。"},
            {"role": "user", "content": f"以下の猫の特徴について、色、柄、猫種を教えてください。\n{descriptions}"}
        ],
        max_tokens=1000,
    )
    return response.choices[0].message.content
    
def main():
    image_dir = 'images'
    output_dir = 'output'

    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    results = []

    for filename in os.listdir(image_dir):
        if filename.lower().endswith(('png', 'jpg', 'jpeg')):
            image_path = os.path.join(image_dir, filename)
            print(f'Processing {image_path}')
            descriptions = analyze_image(image_path)
            details = get_cat_details(descriptions)
            results.append({'file': filename, 'details': details})

    # 結果をMarkdown形式で保存
    output_file = os.path.join(output_dir, 'cat_details.md')
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write("# Cat Details Report\n\n")
        for result in results:
            f.write(f"## File: {result['file']}\n")
            f.write(f"**Details**: {result['details']}\n\n")

if __name__ == "__main__":
    main()
```

## 出力結果
プログラムを実行すると、output/ 配下に.mdファイルができます。
```
$ poetry run python analyze_cats.py  
```
猫の写真は下記からお借りしました。
フリー素材ぱくたそ［ https://www.pakutaso.com ］

### 最もうまくいった(?)例
![nekocyanPAKE4517-434_TP_V4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/aede1087-ec9b-4aa9-07c5-d418668b3a64.jpeg)


出力結果:
**Details**: この猫の特徴について、以下のように解釈できます。

- **色**: グレー
- **柄**: 詳細な柄の情報は提供されていないため、特定不可。ただし、一般的にグレーの猫は無地やタビー（縞模様）であることが多い。
- **猫種**: ドメスティック・ショートヘア（一般的に「雑種」とも呼ばれる家庭猫）

グレーのドメスティック・ショートヘアは非常に一般的で、特別な柄がない場合が多いですが、場合によってはタビー柄が含まれることもあります。


### うまくいかなかった例
![nekocyanPAKE4478-424_TP_V4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506559/e5a37f3d-13ef-cc20-03c2-d481ca917816.jpeg)

出力結果:
**Details**: 申し訳ありませんが、あなたが提供しているリストは猫の一般的な特徴や関連する要素に関するものであり、特定の猫の特徴、色、柄、品種を特定する情報が含まれていません。例えば、猫の色や柄、具体的な猫種名などは含まれていません。

特定の猫の色、柄、品種を知りたい場合は、その猫の具体的な外見についての詳細な情報を提供していただけると、より正確な識別が可能です。たとえば、以下のような情報があると助かります：
- 毛の色（例：白、黒、灰色）
- 毛の柄（例：縞模様、斑点）
- 特徴的な体型や顔の形
- 猫種の候補（例：アメリカンショートヘア、ペルシャ）

情報を詳しく提供していただければ、その範囲内で最も適切な回答をいたします。

---

結果としては、柄や猫種については情報がたりないために正しい結果を得ることができませんでした。
VisionAPIに画像を読み込ませて単語抽出すると、下記のような結果になっていました。(結果は一例)
```
['Cat', 'Felidae', 'Carnivore', 'Small to medium-sized cats', 'Whiskers', 'Fawn', 'Comfort', 'Snout', 'Tail', 'Wood']
```

「猫」「ネコ科」「肉食」「小型〜中型猫」「ひげ」など、抽象度の高い単語が並んでいます。
これらをOpenAIに渡しているので、詳細な結果が得られませんでした。
Vision APIは触り始めたばかりなので、どの程度精度が高いのか不明です。
ただ画像を読み込ませただけなので、プロンプトで得たい結果を指定してあげれば理想に近づくのか、今後検証してみたいと思います。