LLM御三家(OpenAI, Anthropic, Google)による画像比較能力を比較してみた。

## はじめに

　業務でいわゆる基盤の外観検査をAmazone Look out for Visionでやろうとしたら、サービス終了のお知らせがきたので、代わりに生成AIでやらせてみようと思い、LLM御三家の画像比較能力ってどんなもんだろうと思ったので、Gradioでアプリを作って確認してみました。

## 環境構築
　まずはPython環境を用意して、gradio, anthropic, google-generativeai, openaiを使っている環境に合わせて、pipなりpoetryなりuvなどでインストールしてください。あとは各種APIキーを取得しておいて下さい。

## ソースコード

　各APIキーはsecret.jsonに記載、環境変数から読みたい人は適宜変更してください。

```secret.json
{
    "ANTHROPIC_API_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "GEMINI_API_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "OPENAI_API_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
```

　今回使用するモデルは以下の通りです。
- Anthropic: claude-3-5-sonnet-20241022
- Google: gemini-1.5-pro
- OpenAI: gpt-4o

```gr_image_comparison.py
import base64
import json
from typing import Any

import gradio as gr
from PIL import Image
import anthropic
import google.generativeai as genai
from openai import OpenAI

# プロンプトの定義
SYSTEM_PROMPT = "あなたは異常検知に特化した高度な画像分析AIアシスタントです。"
USER_PROMPT = "2つの画像に違いがあれば教えてください。"
# # 将棋の盤面の判定用プロンプト
# USER_PROMPT = "2つの将棋の盤上の駒の配置に注目し、違いがあれば教えて下さい。"


def load_api_key(file_path: str = "secret.json", llm_type: str = "openai") -> str:
    """
    JSONファイルからAPIキーを読み込みます。

    Args:
        file_path (str): APIキーが保存されているJSONファイルのパス。
        llm_type (str): LLMの種類。

    Returns:
        str: 指定されたLLMタイプに対応するAPIキー。

    Raises:
        ValueError: サポートされていないLLMタイプが指定された場合。
    """
    with open(file_path, "r") as f:
        secret = json.load(f)

    key_mapping = {
        "openai": "OPENAI_API_KEY",
        "anthropic": "ANTHROPIC_API_KEY",
        "gemini": "GEMINI_API_KEY",
    }

    if llm_type in key_mapping:
        return secret.get(key_mapping[llm_type], "")
    else:
        raise ValueError("Error: LLM Type is not supported.")


def encode_image_to_base64(image_path: str) -> str:
    """
    画像ファイルをBase64形式にエンコードします。

    Args:
        image_path (str): 画像ファイルのパス。

    Returns:
        str: Base64エンコードされた画像データ。
    """
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")


def generate_image_comparison_request(
    image1_path: str, image2_path: str, llm_type: str
) -> str:
    """
    指定されたLLMを使用して画像比較リクエストを生成し、結果を取得します。

    Args:
        image1_path (str): 比較する最初の画像のパス。
        image2_path (str): 比較する2番目の画像のパス。
        llm_type (str): 使用するLLMの種類。

    Returns:
        str: 画像比較の結果。
    """
    api_key = load_api_key(file_path="secret.json", llm_type=llm_type)
    base64_image_1 = encode_image_to_base64(image1_path)
    base64_image_2 = encode_image_to_base64(image2_path)

    if llm_type == "openai":
        return process_with_openai(api_key, base64_image_1, base64_image_2)
    elif llm_type == "anthropic":
        return process_with_anthropic(api_key, base64_image_1, base64_image_2)
    elif llm_type == "gemini":
        return process_with_gemini(api_key, image1_path, image2_path)
    else:
        return "Error: LLM Type is not supported."


def process_with_openai(api_key: str, base64_image_1: str, base64_image_2: str) -> str:
    """
    OpenAI APIを使用して画像比較を行います。

    Args:
        api_key (str): OpenAIのAPIキー。
        base64_image_1 (str): 最初の画像のBase64データ。
        base64_image_2 (str): 2番目の画像のBase64データ。

    Returns:
        str: OpenAI APIのレスポンス内容。
    """
    openai_client = OpenAI(api_key=api_key)
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {
            "role": "user",
            "content": [
                {"type": "text", "text": USER_PROMPT},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image_1}"}},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image_2}"}},
            ],
        },
    ]
    response = openai_client.chat.completions.create(
        model="gpt-4o", messages=messages, max_tokens=1024, temperature=0
    )
    return response.choices[0].message.content


def process_with_anthropic(
    api_key: str, base64_image_1: str, base64_image_2: str
) -> str:
    """
    Anthropic APIを使用して画像比較を行います。

    Args:
        api_key (str): AnthropicのAPIキー。
        base64_image_1 (str): 最初の画像のBase64データ。
        base64_image_2 (str): 2番目の画像のBase64データ。

    Returns:
        str: Anthropic APIのレスポンス内容。
    """
    anthropic_client = anthropic.Anthropic(api_key=api_key)
    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": USER_PROMPT},
                {"type": "text", "text": "画像1:"},
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": base64_image_1,
                    },
                },
                {"type": "text", "text": "画像2:"},
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": base64_image_2,
                    },
                },
            ],
        }
    ]
    response = anthropic_client.messages.create(
        model="claude-3-5-sonnet-20241022",
        system=SYSTEM_PROMPT,
        messages=messages,
        max_tokens=1024,
        temperature=0,
    )
    return response.content[0].text


def process_with_gemini(
    api_key: str, image1_path: str, image2_path: str
) -> str:
    """
    Gemini APIを使用して画像比較を行います。

    Args:
        api_key (str): GeminiのAPIキー。
        image1_path (str): 最初の画像のパス。
        image2_path (str): 2番目の画像のパス。

    Returns:
        str: Gemini APIのレスポンス内容。
    """
    genai.configure(api_key=api_key)
    model = genai.GenerativeModel(
        model_name="gemini-1.5-pro", system_instruction=SYSTEM_PROMPT
    )
    prompt = USER_PROMPT
    sample_file_1 = Image.open(image1_path)
    sample_file_2 = Image.open(image2_path)
    response = model.generate_content(
        [prompt, sample_file_1, sample_file_2],
        generation_config=genai.GenerationConfig(max_output_tokens=1024, temperature=0),
    )
    return response.text


def main() -> None:
    """
    Gradioインターフェースを設定して起動します。
    """
    with gr.Blocks() as demo:
        gr.Markdown("## 画像比較アプリケーション")
        llm_type = gr.Radio(
            ["openai", "anthropic", "gemini"], label="LLM Type", value="openai"
        )
        with gr.Row():
            image1 = gr.Image(label="画像1をアップロード", type="filepath")
            image2 = gr.Image(label="画像2をアップロード", type="filepath")
        compare_button = gr.Button("比較")
        result = gr.Textbox(label="判定結果")
        compare_button.click(
            generate_image_comparison_request,
            inputs=[image1, image2, llm_type],
            outputs=[result],
        )

    demo.launch()


if __name__ == "__main__":
    main()

```

ターミナルで以下を実施
```
python gr_image_comparison.py
```

　出力されたアドレスにブラウザでアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/0c783ef9-c0a2-3b13-0977-5d6cc3bf47dc.png)

　画像を2枚アップロードして比較ボタンを押せば判定結果に結果が表示されます。

## 今回の検証で使う画像

#### 色違いフィギュア
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/11ceffb0-ccf0-ecdd-0117-bf8df35378c2.jpeg">
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/d3681617-5282-7b21-b0fc-c417cc021689.jpeg">


#### 量の違うアイスティー
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/eeba3537-3d5c-bdc6-667a-55f218009362.jpeg">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/661a241a-133f-394b-e15a-5b8007180468.jpeg">


#### 将棋の盤面
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/7e942a2b-2b42-856a-9633-63b1e614b033.jpeg">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350321/3a81029a-a45d-0642-9d2a-5b0386f8ee50.jpeg">


## LLMの出力結果

#### 色違いフィギュア
- OpenAI
```
2つの画像の違いは、キャラクターの色です。

1枚目の画像:
- キャラクターは紫と白の配色です。

2枚目の画像:
- キャラクターは黄色と白の配色です。

その他のデザインやポーズは同じです。
```

- Anthropic
```
2つの画像の主な違いは色です：

1. 画像1のキャラクターは紫色の帽子と白い体が特徴です。
2. 画像2のキャラクターは白色の帽子とクリーム色がかった体をしています。

ポーズや表情、その他の特徴（黄色い丸いマーク、笑顔、手の形など）は両方の画像で同じように見えます。これは同じフィギュアで色違いバージョンのように見えます。
```

- Google
```
2 つの画像は、同じキャラクターのフィギュアですが、色が異なります。1 つ目のフィギュアは主に青と白で、2 つ目のフィギュアは黄色と灰色です。
```

#### 量の違うアイスティー
- OpenAI
```
2つの画像を比較しましたが、特に目立った違いは見当たりません。どちらの画像も同じように見えます。
```
- Anthropic
```
2つの画像を比較しましたが、大きな違いは見られません。どちらも同じアイスティーのグラスで、以下の共通点があります：

1. 同じサイズのグラスを使用
2. 同じ量の氷と茶色の液体（アイスティー）
3. 同じ白いストロー
4. 同じ白いコースターの上に置かれている
5. 同じ木目調のテーブル上に配置
6. 同じような照明条件

撮影のアングルや明るさにわずかな違いはありますが、基本的に同じ飲み物を撮影した画像だと判断できます。
```
- Google
```
2つの画像は非常に似ていますが、微妙な違いがいくつかあります。高度な画像分析アルゴリズムを用いて検出した違いは以下の通りです。

* **液体の量:** 2枚目の画像では、紅茶の液面がわずかに下がっているように見えます。これは蒸発、または誰かが少し飲んだ可能性があります。
* **氷の位置:** 氷の位置がわずかに変化しています。これは時間の経過による融解、またはグラスがわずかに動いたことが原因と考えられます。
* **ストローの位置:** ストローの角度やグラス内の位置に微妙な違いが見られます。
* **光の反射:** グラスの表面、液体、氷の光の反射にわずかな違いがあります。これはカメラの位置の微妙な変化、または周囲の照明条件の変化によるものと考えられます。

これらの違いは非常に小さく、人間の目では見逃してしまう可能性があります。しかし、ピクセルレベルでの比較を行うことで、これらの微妙な変化を検出することが可能です。


これらの違いが有意なものであるかどうかは、画像の撮影間隔や状況によって異なります。例えば、数分間隔で撮影された画像であれば、これらの変化は自然なものと考えられます。しかし、同じ瞬間に撮影されたとされる画像にこれらの違いがある場合は、画像の改ざんが行われた可能性も考慮する必要があります。
```


#### 将棋の盤面
- OpenAI
```
2つの将棋盤の駒の配置の違いは以下の通りです：

1. **1枚目の画像**:
   - 右側の「歩」が1段目に1枚多く配置されています。

2. **2枚目の画像**:
   - 右側の「歩」が1段目に1枚少なくなっています。

この違いが主な相違点です。その他の駒の配置は同じです。
```
- Anthropic
```
2つの画像を比較すると、以下の違いがあります：

画像2では、画像1と比べて：
1. 左側の歩の駒が1つ下に移動しています（5段目から6段目へ）
2. もう1つの歩の駒も同様に下に移動しています（同じく5段目から6段目へ）

これらの変化以外は、両方の画像で駒の配置は同じように見えます。
```
- Google
```
2つの画像を比較した結果、以下の違いを発見しました。

* **5八の歩の位置:** 1枚目の画像では5八に歩がありますが、2枚目の画像では歩がなくなっています。

他に駒の配置、向き、種類に変更は見られません。盤の向きや光源による影の違いはありますが、駒の状態に変化はありません。

よって、5八の歩の有無が2つの画像の唯一の違いです。
```

## 結果まとめ

|  | OpenAI | Anthropic | Google |
|---|---|---|---|
|フィギュア| ◯ | ◯ | ◯ |
|アイスティー| ✕ | ✕ | ◯ |
|将棋の盤面| △ | △ | △ |

- フィギュア:どのモデルも正しく違いを認識できました
- アイスティー:GoogleのGeminiのみがアイスティーの量とストローの角度の違いを認識し、他のモデルは違いを認識できませんでした
- 将棋の盤面:どのモデルも違いがあることは認識していましたが、指摘内容が正しくありませんでした

## LLMによる画像分析でLLMが苦手とする分野

- 空間的な関係性の把握: 画像内の要素間の空間的な関係を正確に理解することが難しい
- 複雑な図表の理解: 複雑な図表や業務文書の認識は課題となっています(※ただし、ファインチューニングによって図表の理解力が向上したという報告もあります)
- 特定業務領域の専門的な画像解析: 一般的な画像認識は得意ですが、特定の業務に特化した高度な画像解析には課題があります
- 複数の画像を用いた高度なタスク: スライド認識など、複数の画像を用いた複雑なタスクの処理には改善の余地があります


## まとめ

　Gradioでアプリを使ってLLM御三家(OpenAI, Anthropic, GoogleGEMINI)の画像比較能力を比較してみましたが、簡単な画像では正解するものの、複雑な画像になると精度が低くなるようです。AGIを目指すうえで、この分野の発展は欠かせないと思われるので今後の技術進歩に期待です。