RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

LLMによるOCR、画像→構造化データをOpenAI APIで実現【Image inputs × Structured Outputs】

こんにちは。 ノバセルにてデータサイエンティストをしています、石川ナディーム(@nadeemishikawa)です。

本記事ではOpenAI APIの「Image inputs対応モデル」「Structured Outputs」を組み合わせ、1 回の API 呼び出しで「画像 → 指定構造の構造化データ(JSON/Pydantic)」まで完結させる方法を紹介します。

はじめに

ビジネスシーンでは、請求書や名刺など、画像形式の非構造化データを扱う機会が、何かと数多く存在しています。今までは、これらの画像からテキスト情報を抽出するためにはOCR(光学的文字認識)が主に用いられてきましたが、いくつかの実用的な課題がありました。

今までのOCRも便利でしたが、例えば

  • ちょっとレイアウトが違うだけで、うまく読み取れない…
  • 読み取った後、欲しい情報だけを取り出すのが大変(正規表現つらい…)

こうした悩みが実務でよく発生していました。

しかし、最新のマルチモーダルモデルの登場で、こうした課題の多くが解消できるようになってきています。

本記事では、「LLMならではの柔軟な情報抽出」と、「構造化出力」をセットで使う実装ノウハウを、コードとともにご紹介します。

方法

ここからは、OpenAI APIの2つの機能を活用し、どのようにして画像から構造化データを抽出するのかを解説します。

1. Image inputs

Image inputに対応しているモデルは、画像をAPIの入力として渡せます。従来のテキストに加えて、画像も認識できるため、応用範囲がかなり広いです。

従来のOCRでは難しかった柔軟な解釈や、レイアウトに依存しないデータ抽出が実現できます。

たとえば

  • 「この写真に誰が写っている?」
  • 「この表の中身を要約して」

といった質問もできます。

画像はbase64エンコードするか、URLとすることで、APIリクエストに含めることができます。これにより、テキストと画像を組み合わせて指示を送ることができます。

2. Structured Outputs

Structured Outputsに対応しているモデルは、APIの返答を事前に定義したJSONスキーマやPydanticモデルの「型」に沿った構造化データで受け取ることができます。 従来の自由なテキスト出力だけでなく、明確に構造化された形での出力が得られるため、システムへの組み込みが簡単になります。

response_formatにJSON SchemaやPydanticで定義したモデルを指定することで簡単に利用することができます。

※Structured Outputsは、現在OpenAIの一部モデルにて利用可能です

実装例1:名刺から情報抽出

実装例として、まずは業務利用でも頻出の「名刺OCR」を題材に、名刺画像から記載情報を抽出し、構造化データとして出力するプロセスを解説します。

入力画像

異なるレイアウトの名刺画像(下図2枚)を用意しました。

(名刺画像はGeminiのCanvasを利用して、架空の名刺を生成したものです)

横長画像
画像A
縦長画像
画像B

コードとプロンプト設計

下記に示すPythonコードは、指定された名刺画像を読み込み、OpenAI APIを介して情報抽出を行う一連の処理を実装したものです。 コードにおいて重要な要素は、出力データ構造を定義するPydanticで定義したBusinessCardクラスと、モデルに入力するSYSTEM_PROMPTです。

コードの要点

  1. System Prompt に抽出過程を言語化し明確に指示

    ※プロンプトエンジニアリングについては本稿では、深く取り扱わないですが、公式が出している以下の記事がおすすめです。

    汎用的なもの platform.openai.com GPT-4.1でのプロンプトエンジニアリングについて、より深掘りしたもの cookbook.openai.com

  2. Structured Outputs関連

    1. pydanticを用いて、BusinessCardクラスを定義し、response_formatを指定
    2. 読み取れないフィールドはNoneとすることをプロンプト内で明確化、推測は禁止する
      1. 型指定で既に、Optional に設定しているが、このプロンプトを入れることで、読み取り確度が低い場合にハルシネーションを起こし、それっぽい値を出力をするという現象を一定防ぐ効果があります。
      2. また、読み取れなかった場合の挙動を明確にする意図もあります。空文字とNoneが入り混じってしまう等を防ぐ意味合いです。
    3. 複数項目がある場合は、コンマ区切りで文字列として読み取る(実運用ではリストで受け取ることを定義する場合もあります)
from pydantic import BaseModel
from typing import Optional
from openai import OpenAI

# 画像をbase64形式にエンコードする関数
def encode_image(image_path: str) -> str:
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

class BusinessCard(BaseModel):
    person_name_ja: Optional[str]
    person_name_en: Optional[str]
    job_title: Optional[str]
    company: Optional[str]
    email: Optional[str]
    phone_numbers: Optional[str]
    website: Optional[str]
    address: Optional[str]
    
# インプット画像へのパス
image_path = "path-to-your-image"

# APIに入力する画像をbase64形式にエンコード
base64_image = encode_image(image_path)

SYSTEM_PROMPT = """
# Role and Objective
あなたは名刺画像から情報を抽出する、OCRおよび情報抽出の専門エージェントです。  
与えられた名刺画像から必要な情報を構造化データとして正確に抽出することがあなたのミッションです。

# Instructions
- 名刺画像から抽出できる全ての情報を取得してください。
- 指定されたresponse_formatに完全に一致する結果を出力してください。
- **読み取れない、もしくは存在しないフィールドは必ずNoneにしてください**。
- キーの追加・削除・名前変更は禁止です。
- 読み取り項目の出力以外(コメント、説明文、マークダウン、推論過程など)は絶対に含めないでください。

## Sub-categories for more detailed instructions
- 氏名や役職など、明確に判別できるフィールドは可能な限り抽出し、正確に記述してください。
- 連絡先(メールアドレス、電話番号など)は、画像から読み取れる全てを対象にしてください。
- 英語・日本語表記が混在している場合は両方記載してください。
- 番号・URL・住所なども、可読な範囲で正確に記載してください。
- 複数のフィールドが存在する場合は、それぞれのフィールドをカンマで区切って文字列として出力してください。

# Reasoning Steps
1. 画像内の全情報を正確に読み取る。
2. response_formatに沿って各フィールドを正しく抽出する。
3. 抽出できなかった場合はNoneとする。
4. 必要な情報のみをresponse_format通りに出力する。

# Output Format
出力は以下の形式で行ってください。

## Example
{
  "person_name_ja": "山田太郎",
  "person_name_en": "Taro Yamada",
  "job_title": "営業部長",
  "company": "株式会社ABC, ABC Inc.",
  "email": "taro.yamada@example.com",
  "phone_numbers": ["03-1234-5678"],
  "website": "https://www.example.com",
  "address": "東京都千代田区1-2-3"
}

# Context
名刺には日本語・英語で記載された名前や会社名、複数の電話番号や連絡先、住所などが記載されています。
実際の画像内容に忠実に、抜けや漏れがないように情報を抽出してください。

# Final instructions and prompt to think step by step
- 出力前に、あなた自身の中で抽出した情報が要求に完全に合致しているかを必ず確認してください。
- 手順を内部で計画し、思考を整理してから最終出力を行ってください(ただし推論過程は絶対に出力しないこと)。
- 出力はresponse_formatに沿った形式で行ってください。
- 抽出できなかった場合は,Noneとしてください。推測で回答しないこと。
"""

USER_TEXT = "この名刺画像から全ての情報を抽出してください。読み取れない、存在しない項目はNoneにしてください。"
)

def extract(image_path: str):
    base64_image = encode_image(image_path)

    client = OpenAI()
    messages = [{
            "role": "system",
            "content": [
                {"type": "text", "text": SYSTEM_PROMPT},
            ]
        },
        {
            "role": "user",
            "content": [
                {"type": "text", "text": USER_TEXT},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
            ]
        }
    ]
    response = client.beta.chat.completions.parse(
        model="gpt-4.1-2025-04-14",
        messages=messages,
        response_format=BusinessCard,
    )
    output = response.choices[0].message.parsed
    return output

result = extract(image_path)
for key, value in result.model_dump().items():
    print(f"{key}: {value}")

画像Aの結果:

person_name_ja: 楽刷 太郎
person_name_en: None
job_title: CXO
company: Novasell Inc., ノバセル株式会社
email: taro.raksul@xxx.com
phone_numbers: 03-1234-5678
website: https://novasell.com
address: 〒106-0041 東京都港区麻布台XXX

見事に、すべての要素が正しく認識されました!

画像Aにはperson_name_en(英語名)の記載がないため、出力結果ではNoneとなっています。 company(会社名)も日本語・英語どちらも正しく抽出され、カンマ区切りで抽出されています。

このように、プロンプトの制約部分も忠実に守られていることが確認できました。

画像Bの結果:

person_name_ja: 楽刷 太郎
person_name_en: Taro Raksul
job_title: CXO
company: 株式会社ノバセル, Novasell Inc.
email: taro.raksul@xxx.com
phone_numbers: 03-1234-5678
website: https://novasell.com
address: 東京都港区麻布台XXX

画像Aでも同様に、レイアウトが全く異なるにも関わらず、惑わされることなく、ちゃんと項目を理解し、同じ形式の正しいデータにしてくれています。

このレイアウトによらない、頑健性がこの方法の強みといえます。

応用編:路線図から「今いる駅」を読み取る

ここからは、さらに一歩進んだ応用例です。 「画像内の状態や関係性」といった、従来のOCRでは難しかった抽象的な概念抽出もLLMなら可能です。これはかなり応用範囲が広く、通常のOCRではできなかったことが可能となる大きな特徴の一つです。

応用例では、視覚的概念を読み取る例として、路線図写真から「現在いる駅名」を抽出するケースを扱います。

入力画像

異なる、レイアウトの路線図を用意しました。

横長画像
画像A
縦長画像
画像B

コードとプロンプト設計(一部抜粋)

class Station(BaseModel):
    station_name: Optional[str]

SYSTEM_PROMPT = """
# Role and Objective
あなたは駅案内画像から現在いる駅を出力する、専門のエージェントです。
現在いる駅名を出力することがあなたのミッションです。

# Instructions
- 画像内の情報から、現在いる駅名のみを特定してください。
- 現在いる駅は、案内図の中で「赤丸+赤字」で強調されています。この特徴をもとに判定してください。
- 出力は駅名(日本語)1つのみとしてください。
- 曖昧な場合は出力せず、Noneとしてください。
- 出力は駅名のみ。他のコメントや説明、形式、推論過程などは絶対に含めないでください。

# Reasoning Steps
1. 画像内の案内図から「赤丸+赤字」で表現されている駅を特定する。
2. その駅名のみを出力する。
3. 複数該当や不明な場合はNoneとする。

# Output Format
出力は以下の形式で行ってください。

## Example
{
  "station_name": "目黒駅"
}

# Final instructions and prompt to think step by step
- 出力前に、必ず上記条件に合致しているかを自身で確認してください。
- 出力は駅名1つのみ。それ以外は絶対に含めないこと。
"""

USER_TEXT = "この画像から現在いる駅名を出力してください。"

def extract(image_path: str):
    base64_image = encode_image(image_path)

    client = OpenAI()
    messages = [
        {
            "role": "system",
            "content": [
                {"type": "text", "text": SYSTEM_PROMPT},
            ]
        },
        {
            "role": "user",
            "content": [
                {"type": "text", "text": USER_TEXT},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
            ]
        }
    ]
    response = client.beta.chat.completions.parse(
        model="gpt-4.1-2025-04-14",
        messages=messages,
        response_format=Station,
    )
    output = response.choices[0].message.parsed
    
    return output

result = extract(image_path)
print(result.station_name)

画像A・Bの結果:

神谷町

画像A・Bでは、全くレイアウトや情報量の違うものであるにも関わらず、両方で期待した出力を得ることができました。

従来のOCR技術では、画像内の全駅名をリスト化することは可能であっても、「現在地」を示す記号と特定の駅名とを関連付けて認識することは極めて困難でした。

しかし、マルチモーダルモデルの発展により抽象的な指示に基づく情報抽出が実現できます。応用例は一要素で簡単なものでしたが、「路線名」「現在駅」「次駅」といったスキーマを定義して、Structured Outputsを用いることで、モデルは画像内の要素間の関係性を解釈し構造化されたデータを返すことも可能です。

実務におけるプロンプト設計のコツ

先に共有した公式のプロンプトエンジニアリングの記事に加えて、良いプロンプトを作成するためには、「自分がどのように情報を認識しているのか」を明確に言語化することが不可欠です。 特に、以下がプロンプトを作成する上で大事だと考えています。

  • 普段自分が無意識に行っている認識や判断のプロセスを洗い出し、それをプロンプトの指示に具体的に反映すること
  • 無意識に行っている動作や判断であっても、ひとつひとつを明確な言葉として表現し、モデルに指示を与えること

この「言語化」の能力がプロンプト設計において非常に重要となります。無意識的に行っている認識プロセス・業務プロセスを言語化することで、モデルが画像や情報を適切に解釈するようになり、精度の高い出力を得られるようになります。

まとめ

ここまでお読みいただきありがとうございました! LLM×画像認識の組み合わせは、これまで手間だった一連の工程をワンステップで自動化できます。以下が、本記事のポイントです。

  • 従来のOCRの限界を突破: レイアウトや記載内容の違いにも強く、柔軟に情報を取得可能
  • 即・構造化データ化: 1API呼び出しで欲しい形式にそのまま変換
  • 業務にあわせて自由に拡張可能: JSONやPydantic形式をカスタマイズして使える
  • 文脈・関係性の理解も得意: 画像内の意味・関係まで抽出できる

名刺や帳票、地図、UIキャプチャ、TVCMや広告クリエイティブの要素抽出など、定型・非定型を問わず多くの業務で活用可能です。 使い方しだいで応用方法は無限大かと思います、ぜひ一度お試しください!