見出し画像

GPT-4oは画像の座標情報を理解しているのか?

こんにちは。朝日新聞社メディア研究開発センターの嘉田です。

早速ですが、みなさんはGPT-4oを使っていますか?
GPT-4oは画像認識精度も上がっていて日本語OCRもできる!と評判ですが、バウンディングボックスも出力できるのか?そもそもGPT-4oは画像の座標情報をどの程度扱えるのだろうか?と疑問に思い、検証することにしました。


検証方法

正しい座標を出力できるか、与えた座標を理解できているか、物体間の位置関係を理解できているか、という観点で確認するべく、下記の3つの方法で検証しました。

  • OCRにおいて、テキストとバウンディングボックスの座標を出力させる … 検証①

  • バウンディングボックスの座標を与えて、対象領域のテキストを出力させる … 検証②

  • 2つのテキストを与えて、距離や方向を出力させる … 検証③

検証コードはこちらを参考にしています。なお、APIキーの取得等についてはここでは割愛します。

使用する画像

こちらの画像で検証しました。画像サイズは幅1292 ×高さ 858ピクセルです。

嘉田の「夏休みのしおり」

正解座標の取得

正解の座標を取得するため、GoogleのCloud Vision APIでOCRを行いました。

今回扱うバウンディングボックスは [x, y, w, h] の形式にしています。
x:バウンディングボックスの左上のx座標
y:バウンディングボックスの左上のy座標
w:バウンディングボックスの幅
h:バウンディングボックスの高さ
※画像左上を原点とする

OCR結果は下記の通りです。流石はGoogle、テキストはノーミスです。

[
    {"text": "夏休みのしおり", "bbox": [336, 52, 607, 82]},
    {"text": "☆しゅくだい", "bbox": [47, 250, 238, 37]},
    {"text": "・仕事", "bbox": [63, 311, 104, 35]},
    {"text": "・家事", "bbox": [62, 377, 104, 33]},
    {"text": "・猫の世話", "bbox": [62, 438, 184, 35]},
    {"text": "☆やくそく", "bbox": [48, 566, 190, 36]},
    {"text": "・電気代を気にせずエアコンをつける", "bbox": [64, 628, 652, 37]},
    {"text": "・外に出るときは日焼け止めを塗る", "bbox": [64, 692, 614, 35]},
    {"text": "・アイスは1日1個まで、我慢できなかったら2個まで", "bbox": [63, 753, 926, 38]},
]

バウンディングボックスを画像に描画してみると下記の通りです。バッチリですね。


[検証①] OCRでの座標出力

最初の検証です。
GPT-4oにテキストとバウンディングボックスの座標を出力させ、どの程度正確か確認します。コード・プロンプトは下記の通りです。

import base64
import requests


def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")


api_key = "hogehoge"
image_path = "sample.png"
base64_image = encode_image(image_path)
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
payload = {
    "model": "gpt-4o",
    "messages": [
        {
            "role": "system",
            "content": "あなたは画像から日本語のテキストを認識するOCRマシンです。提供された画像からすべての日本語テキストを抽出し、テキスト、各テキスト領域のバウンディングボックス、および提供された画像のサイズを含むJSON形式で出力します。バウンディングボックスは左上を原点とする座標系で、ボックスの左上のx座標(左からの距離)、ボックスの左上のy座標(上からの距離)、ボックスの幅、ボックスの高さを含みます。出力形式は以下の通りです。\n\n{\n  \"text\": \"<抽出された全テキスト>\",\n  \"bounding_boxes\": [\n    {\n      \"text\": \"<検出されたテキスト>\",\n      \"bbox\": [<x座標>, <y座標>, <幅>, <高さ>]\n    },\n    ...\n  ],\n  \"image_size\": {\n    \"width\": <画像の幅>,\n    \"height\": <画像の高さ>\n  }\n}",
        },
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "この画像から日本語のテキストを抽出してください。指定されたJSON形式のみを返してください。"},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}},
            ],
        },
    ],
    "max_tokens": 1024,
}
response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
print(response.json())

結果は下記の通りです。テキストは100点です💮

{
  "text": "夏休みのしおり\n☆しゅくだい\n・仕事\n・家事\n・猫の世話\n☆やくそく\n・電気代を気にせずエアコンをつける\n・外に出るときは日焼け止めを塗る\n・アイスは1日1個まで、我慢できなかったら2個まで",
  "bounding_boxes": [
    {
      "text": "夏休みのしおり",
      "bbox": [358, 36, 193, 43]
    },
    {
      "text": "☆しゅくだい",
      "bbox": [73, 125, 100, 18]
    },
    {
      "text": "・仕事",
      "bbox": [73, 159, 44, 18]
    },
    {
      "text": "・家事",
      "bbox": [73, 187, 43, 18]
    },
    {
      "text": "・猫の世話",
      "bbox": [73, 215, 81, 18]
    },
    {
      "text": "☆やくそく",
      "bbox": [73, 257, 100, 18]
    },
    {
      "text": "・電気代を気にせずエアコンをつける",
      "bbox": [73, 291, 288, 18]
    },
    {
      "text": "・外に出るときは日焼け止めを塗る",
      "bbox": [73, 319, 235, 18]
    },
    {
      "text": "・アイスは1日1個まで、我慢できなかったら2個まで",
      "bbox": [73, 346, 361, 18]
    }
  ],
  "image_size": {
    "width": 1156,
    "height": 768
  }
}

バウンディングボックスの前に画像サイズを確認すると、比率は保ちつつもオリジナルから変更してしまっています。
  オリジナル:幅1292 ×高さ 858
  出力結果:幅1156×高さ 768
画像サイズを変更しないようにプロンプトを調整しても結果は変わらずだったので、諦めてそのままにしています。

出力された座標を確認するため、画像を幅1156×高さ 768にリサイズした上でバウンディングボックスを描画しました。

全然ダメでした。位置関係や幅・高さはいい線いってるので、画像サイズについてや座標系のルールを充実させるともう少しうまくいくのかも?

[検証②] 座標を与えてOCR

続けて、バウンディングボックスの座標を与えて対象領域を指定し、その領域のテキストを出力させます。これによって与えたバウンディングボックスの座標を理解できているか確認します。コード・プロンプトは下記の通りです。

import base64
import requests


def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")


api_key = "hogehoge"
image_path = "sample.png"
base64_image = encode_image(image_path)
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
payload = {
    "model": "gpt-4o",
    "messages": [
        {
            "role": "system",
            "content": "あなたは画像から日本語のテキストを認識するOCRマシンです。提供された画像の特定の領域に書かれているテキストを認識し、出力します。バウンディングボックスは左上を原点とする座標系で、ボックスの左上のx座標(左からの距離)、ボックスの左上のy座標(上からの距離)、ボックスの幅、ボックスの高さを含みます。出力形式は以下の通りです。\n\n{\"text\": \"<対象領域のテキスト>\"}",
        },
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "この画像から対象領域の日本語のテキストを抽出してください。\n対象領域のバウンディングボックス:[336, 52, 607, 82]\n指定されたJSON形式のみを返してください。"},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}},
            ],
        },
    ],
    "max_tokens": 1024,
}
response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
print(response.json())

与えたのは「夏休みのしおり」のバウンディングボックスです。
> {"text": "夏休みのしおり", "bbox": [336, 52, 607, 82]}

結果は下記の通りです。なんと正解しました💮

{"text": "夏休みのしおり"}

いざ正解すると「本当か?」と思ってしまうので、他の例でも試してみると…

ターゲット: [64, 628, 652, 37]
正解: {"text": "・電気代を気にせずエアコンをつける"}
出力: {"text": "・アイスは1日1個まで、我慢できなかったら2個まで"}

ターゲット: [47, 250, 238, 37]
正解: {"text": "☆しゅくだい"}
出力: {"text": "・仕事"}

ダメでした。近いエリアのテキストを返してはいますが、正確な座標を扱えているわけではなさそうです。

[検証③] 2つのテキストの位置関係分析

最後に、物体間の位置関係を理解できているか確認するため、2つのテキストを与えて距離や方向を出力させてみます。コード・プロンプトは下記の通りであり、距離、方向の定義はプロンプト中に記載しています。これまでの検証でバウンディングボックスがうまく扱えていなかったので、距離については中心点を出力させることで算出しています(駄目元)。

import base64
import requests


def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")


api_key = "hogehoge"
image_path = "sample.png"
base64_image = encode_image(image_path)
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
payload = {
    "model": "gpt-4o",
    "messages": [
        {
            "role": "system",
            "content": "あなたは画像内のテキストの相対的な位置関係を分析するAIアシスタントです。提供された画像に書かれている2つのテキストを認識し、距離、距離算出に使用したテキストの中心点の座標、方向、および提供された画像のサイズを含むJSON形式で出力します。テキストの中心点は左上を原点とする座標系で表します。その情報を元に、2つのテキストの中心点間の距離をピクセル単位で算出します。方向はテキスト1を基準としてテキスト2がどの方向にあるかを8方位(北、北東、東、南東、南、南西、西、北西)で表します。出力形式は以下の通りです。\n\n{\"center1\": [<テキスト1の中心のx座標>, <テキスト1の中心のy座標>], \"center2\": [<テキスト2の中心のx座標>, <テキスト2の中心のy座標>], \"distance\": <距離>, \"direction\": \"<方向>\", \"image_size\": {\n    \"width\": <画像の幅>,\n    \"height\": <画像の高さ>\n  }\n}",
        },
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "指定された2つのテキストの距離と方向を分析してください。\nテキスト1:☆しゅくだい\nテキスト2:☆やくそく\n指定されたJSON形式のみを返してください。"},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}},
            ],
        },
    ],
    "max_tokens": 1024,
}
response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
print(response.json())

2ペアで試した結果が下記です。

ターゲット: テキスト1「☆やくそく」、テキスト2「夏休みのしおり」
出力: {
    "center1": [90, 299],
    "center2": [309, 85],
    "distance": 298,
    "direction": "北東",
    "image_size": {"width": 1156, "height": 768},
}
ターゲット: テキスト1「☆しゅくだい」、テキスト2「☆やくそく」
出力: {
    "center1": [60, 149],
    "center2": [60, 408],
    "distance": 259,
    "direction": "南",
    "image_size": {"width": 1156, "height": 768},
}

そもそも「☆やくそく」の中心座標が2例で異なっています。distanceの計算も合っていたり合っていなかったりです。画像サイズの違いも考慮して正解の距離と出力された距離を比較しましたが、惜しくもなかったです(計算式は省略)。中心点の定義をもう少し丁寧にすれば改善するかもしれません。
一方で、方向は正しく出力できています。他のペアでも試したところ、方向は比較的安定して出力できており、プロンプトを無視して16方位で出力するようなケースもありましたが、真反対を出力することなどはありませんでした。


おわりに

ということで今回の検証においては、GPT-4oは画像の座標情報をうまく扱ってくれませんでした。今後のアップデートに期待です。一方で、テキスト自体や物体間の方向はうまく取得できるという結果になりました。座標情報が不要な場合には、簡単にOCRができる基盤として活躍しそうです。
また、今回はOCRを中心に検証しましたが、物体検出などでも結果が気になるところです。

(メディア研究開発センター・嘉田紗世)