Eagle + キャプション生成API（Azure AI Vision）で画像管理


Eagleという画像管理ソフトウェアのAPIを使ってみた記事です。
## Eagleとは
画像管理ソフトウェアで、画像に限らず動画や音声、pdfなども扱えてとても便利です。
また、APIによる操作も可能で、機械的な画像整理に役立てることができます
有料のソフトですが、30日間は無料で触れます。

https://jp.eagle.cool/

Eagleで利用できるAPI一覧はこちらに記載されています。

https://api.eagle.cool/

例えば、、、
- ```GET /api/item/list```
こちらは各種フィルタを指定して、Eagle上に保存している画像の一覧を取得できます。
画像管理の場合は、このAPIで画像を取得してほかの操作へつなげることが多くなると思います。
- ```GET /api/item/info```
こちらは画像のファイル名、タグ、分類、フォルダー、サイズといった情報を取得できます。
- ```GET /api/item/thumbnail```
こちらは画像データが格納されているファイルパスを取得できます。
- ```POST /api/item/update```
画像の情報を更新できます。タグや注釈の内容を変更できます。

使い方のサンプルはこのような感じ
Eagleを起動しておくと、「localhost:41595」でAPIサーバも起動して以下のようにリクエストを送ることができます。
```sample.py
import requests
import json

# 検索パラメータ
params = {
    "limit": 1,
    "ext": "png",
    "orderBy": 'FILESIZE'
}

url = 'http://localhost:41595/api/item/list'
res = requests.get(url, params)

items = res.json()
print(items)
```

<details><summary>実行結果</summary>

```
{
    "status": "success",
    "data": [
        {
            "id": "LTE0376T8JYKA",
            "name": "BattleChara_Dead",
            "size": 83422,
            "btime": 1708248470204,
            "mtime": 1662305046030,
            "ext": "png",
            "tags": [],
            "folders": [],
            "isDeleted": false,
            "url": "",
            "annotation": "黒地に灰色の墓石,灰色の墓石に十字架,手の白黒画像,黒地に灰色の墓石,グレー地に白十字,灰色の墓石に十字架,グレー地に白文字,グレー地に白抜き文字,グレー地に白文字",
            "modificationTime": 1709620809513,
            "height": 1000,
            "width": 1000,
            "noThumbnail": true,
            "palettes": [
                {
                    "color": [
                        108,
                        108,
                        108
                    ],
                    "ratio": 46
                },
                {
                    "color": [
                        100,
                        100,
                        100
                    ],
                    "ratio": 14
                },
                {
                    "color": [
                        45,
                        45,
                        45
                    ],
                    "ratio": 13
                },
                {
                    "color": [
                        128,
                        128,
                        128
                    ],
                    "ratio": 10
                },
                {
                    "color": [
                        68,
                        68,
                        68
                    ],
                    "ratio": 4.39
                },
                {
                    "color": [
                        76,
                        76,
                        76
                    ],
                    "ratio": 3.38
                },
                {
                    "color": [
                        60,
                        60,
                        60
                    ],
                    "ratio": 3.08
                },
                {
                    "color": [
                        92,
                        92,
                        92
                    ],
                    "ratio": 1.87
                },
                {
                    "color": [
                        84,
                        84,
                        84
                    ],
                    "ratio": 1.06
                },
                {
                    "color": [
                        115,
                        116,
                        116
                    ],
                    "ratio": 0.96
                }
            ],
            "lastModified": 1709620988997
        }
    ]
}
```
</details>


## キャプション生成とは
キャプションとは、画像データにつける短い説明文のことです。
これを自動生成するものがキャプション生成と呼ばれています。

キャプション生成に関するサービスもいくつかありますが、今回はMircorosoft AzureのAzure AI Visionというサービスを使ってキャプション生成してみたいと思います。
公式ページでは、以下の画像でキャプション生成をしたサンプルがあります。

https://learn.microsoft.com/ja-jp/azure/ai-services/computer-vision/concept-describe-images-40?tabs=image
> <img width=70% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706027/87d282ef-b213-12da-7d20-be5af07241bd.png>
>
> ```.json
> "captions": [
>     {
>         "text": "a man pointing at a screen",
>         "confidence": 0.4891590476036072
>     }
> ]
> ```

## Eagleに保存した画像のキャプションを生成する
ざっくりとした機能構成は以下になります。
<img width=70% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706027/578a0f3f-6878-5f92-2666-91ad98e96a5c.png>

#### ①画像データ取得
まずEagle側で保存されている画像を`GET /api/item/list`で一覧取得し、それぞれからIDを取り出します。その後、IDをキーに`GET /api/item/thumbnail`で画像のパスを取得します。
また、画像データはAzure AI Visionに渡すため、データをbase64エンコードします。
```app.py
import requests
import azure.ai.vision as sdk
import os
from dotenv import load_dotenv
import json
import urllib.parse

# ・・・（中略）

def get_item_list(item_list, next_offset=0):
    res_data = item_list
    # 一覧取得する条件（今回はすべての画像を再帰的に取得する）
    params = {
        "limit": 100,
        "offset": next_offset
    }

    url = 'http://localhost:41595/api/item/list'
    res = requests.get(url, params)

    items = res.json()

    for item in items['data']:
        res_data.append(item['id'])

    # 最大数の画像を取得したら再帰呼び出し
    if len(items['data']) == 100:
        print("next offset call")
        return get_item_list(res_data, next_offset+1)
    else:
        print(len(res_data))
        return res_data
    
def get_thumbnail(id):
    # IDを指定してサムネイルを取得
    params = {
        "id": id
    }
    url = 'http://localhost:41595/api/item/thumbnail'
    res = requests.get(url, params)

    thumbnail_path = res.json()['data']
    
    return thumbnail_path
```

#### ②キャプション生成
次に、エンコードしたデータをAzure AI Visionに渡してキャプションを生成してもらいます。
①で生成

```app.py
import requests
import azure.ai.vision as sdk
import os
from dotenv import load_dotenv
import json
import urllib.parse

# ・・・（中略）

def create_caption(path):
    # ここのpathはget_thumbnailで取得したEagle上の画像保存先
    full_path = urllib.parse.unquote(path, encoding='utf-8')
    print("full path: {}".format(full_path))
    image_buffer = None

    # 画像ファイルを開く
    with open(full_path, 'rb') as image_file:
        image_buffer = image_file.read()
    image_source_buffer = sdk.ImageSourceBuffer()
    image_source_buffer.image_writer.write(image_buffer)
    vision_source = sdk.VisionSource(image_source_buffer=image_source_buffer)
    image_analyzer = sdk.ImageAnalyzer(service_options, vision_source, analysis_options)
    result = image_analyzer.analyze()

    # 処理完了したら、キャプション部分を抽出する
    if result.reason == sdk.ImageAnalysisResultReason.ANALYZED:
        if result.dense_captions is not None:
            print(" Dense Captinos:")
            for caption in result.dense_captions:
                print("  '{}', {}, Confidence: {:.4f}".format(caption.content, caption.bounding_box, caption.confidence))


        result_details = sdk.ImageAnalysisResultDetails.from_result(result)

        return json.loads(result_details.json_result)

    # こちらはエラー処理
    else:
        error_details = sdk.ImageAnalysisErrorDetails.from_result(result)
        print(" Analysis failed.")
        print("   Error reason: {}".format(error_details.reason))
        print("   Error code: {}".format(error_details.error_code))
        print("   Error message: {}".format(error_details.message))
```

#### ③キャプション翻訳
サンプルにある通り、生成されたキャプションは英語なのでDeepLのAPIも利用して日本語訳します。利用にはAPIキーが必要なので、別途取得します。
(こちらが参考になると思います。[DeepL API を使ってみる](https://qiita.com/rihok/items/962890b4b86ea052e54c))
```app.py
params = {
    'auth_key': deepl_key,
    'text': data['text'],
    'source_lang': 'EN',
    'target_lang': 'JA'
}
translate_req = requests.post("https://api-free.deepl.com/v2/translate", data=params)
translate_result = translate_req.json()
```

#### ④データ更新
最後に、日本語訳されたキャプションをEagleの画像にコメントとして追加します。
```app.py
# annotationに翻訳されたキャプションが入っている
update_params = {
    "id": id,
    "annotation": annotation
}

print("update_params: ", update_params)

update_req = requests.post("http://localhost:41595/api/item/update", json=update_params)
```

生成されたキャプションがこのようにコメント欄に追加されました。
![スクリーンショット 2024-03-05 154540.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706027/0c6b9043-a289-ef59-ac63-14ddb32e866b.png)


## おわりに
今回は画像管理ソフトウェアEagleのAPIを使って画像のキャプション生成を試してみました。
注釈欄に特徴を表す単語が入るので、検索欄で探しやすくなるというメリットがありました。

ただ、特殊な画像（特定のキャラクターイラストでキャラ名を入れる、創作など現実にないもの）のキャプション生成は難しいと思います。生成されるキャプションは一般的な単語を使っているようなので、そういった特殊な要素の抽出には、また別の方法を検討しないといけないと思いました。


<details><summary>コード全体はこちら</summary>

```app.py
import requests
import azure.ai.vision as sdk
import os
from dotenv import load_dotenv
import json
import urllib.parse

load_dotenv()

azure_endpoint = os.environ['AZURE_ENDPOINT']
azure_subscription_key = os.environ['AZURE_SUBSCRIPTION_KEY']
deepl_key = os.environ['DEEPL_API_KEY']

service_options = sdk.VisionServiceOptions(azure_endpoint, azure_subscription_key)
analysis_options = sdk.ImageAnalysisOptions()
analysis_options.features = {
    sdk.ImageAnalysisFeature.DENSE_CAPTIONS
}
analysis_options.language = "en"

def get_item_list(item_list, next_offset=0):
    res_data = item_list
    params = {
        "limit": 100,
        "offset": next_offset
    }

    url = 'http://localhost:41595/api/item/list'
    res = requests.get(url, params)

    items = res.json()

    for item in items['data']:
        res_data.append(item['id'])

    if len(items['data']) == 100:
        print("next offset call")
        return get_item_list(res_data, next_offset+1)
    else:
        print(len(res_data))
        return res_data
    
def get_thumbnail(id):
    params = {
        "id": id
    }
    url = 'http://localhost:41595/api/item/thumbnail'
    res = requests.get(url, params)

    thumbnail_path = res.json()['data']
    
    return thumbnail_path

def create_caption(path):
    full_path = urllib.parse.unquote(path, encoding='utf-8')
    print("full path: {}".format(full_path))
    image_buffer = None
    with open(full_path, 'rb') as image_file:
        image_buffer = image_file.read()
    image_source_buffer = sdk.ImageSourceBuffer()
    image_source_buffer.image_writer.write(image_buffer)
    vision_source = sdk.VisionSource(image_source_buffer=image_source_buffer)
    image_analyzer = sdk.ImageAnalyzer(service_options, vision_source, analysis_options)
    result = image_analyzer.analyze()

    if result.reason == sdk.ImageAnalysisResultReason.ANALYZED:

        if result.dense_captions is not None:
            print(" Dense Captinos:")
            for caption in result.dense_captions:
                print("  '{}', {}, Confidence: {:.4f}".format(caption.content, caption.bounding_box, caption.confidence))


        result_details = sdk.ImageAnalysisResultDetails.from_result(result)

        return json.loads(result_details.json_result)

    else:
        error_details = sdk.ImageAnalysisErrorDetails.from_result(result)
        print(" Analysis failed.")
        print("   Error reason: {}".format(error_details.reason))
        print("   Error code: {}".format(error_details.error_code))
        print("   Error message: {}".format(error_details.message))

if __name__ == '__main__':
    item_ids = get_item_list([], 0)
    count = 0
    for id in item_ids:
        print("create caption id:{}".format(id))
        path = get_thumbnail(id)
        result = create_caption(path)
        caption = []

        for data in result['denseCaptionsResult']['values']:
            params = {
                'auth_key': deepl_key,
                'text': data['text'],
                'source_lang': 'EN',
                'target_lang': 'JA'
            }
            translate_req = requests.post("https://api-free.deepl.com/v2/translate", data=params)
            translate_result = translate_req.json()
            caption.append(translate_result['translations'][0]['text'])

        annotation = ",".join(caption)
        
        update_params = {
            "id": id,
            "annotation": annotation
        }

        print("update_params: ", update_params)

        update_req = requests.post("http://localhost:41595/api/item/update", json=update_params)
        print(update_req.text)
```


</details>