LLMを活用したホワイトボード画像からのテキスト情報抽出と情報蓄積について

に公開

はじめに

会議やディスカッションの場で、ホワイトボードにさまざまなアイデアや図を描きながら議論を進めたものの、最終的には写真を撮って保存するだけで終わってしまった...
そんな経験がある方も多いのではないでしょうか。

会議やブレインストーミングでは、ホワイトボードを使って図表や箇条書きで情報を整理し、議論の内容を視覚的にまとめるのが一般的です。そうして生まれるアイデアや意思決定の記録は、チームにとって非常に価値のあるナレッジとなり得ます。

しかし、実際には、そうした情報はスマートフォンに保存されたままになってしまい、あとで見返したり活用されることはあまりないのが実情です。また、いざ振り返ろうとしても、「どこに保存したのか分からない」「文字がかすれて読みづらい」「そもそも何を議論していたか思い出せない」といった障害が立ちはだかり、貴重な情報が事実上失われてしまうのです。

そこで本検証では、ホワイトボードなどをはじめとする画像からOCR(光学文字認識)の技術を用いてテキスト情報を抽出し、その結果を構造化してデータベースに保存することを試みます。
これにより、これまで画像として保存されるだけだった情報を、後から検索・再利用しやすい形で蓄積できるようになることを目指します。

さらに今回は、抽出されたテキストデータをベクトルデータベースに格納し、意味的な類似度にもとづく柔軟な検索ができる仕組みの構築も行います。これによって、単なるキーワード検索ではなく、「文脈的に近い情報を探す」といったことも可能になります。例えば、特定のキーワードを覚えていなくても、それに近い単語さえ覚えていたら、自身が求めるデータに効率よくアクセスできます。

本記事の対象は以下の通りです。

  • Python初学者~中級者
  • ホワイトボード画像をはじめとする画像のOCRに興味のある方
  • ベクトル検索やベクトルデータベースに興味のある方

用語の説明

ここでは、本記事で扱う重要用語の説明を簡潔に行います。

  • OCR(光学文字認識)
    画像やPDFに含まれる文字情報を検出し、機械が読み取れるテキストデータに変換する技術。印刷文字や手書き文字をデジタル化する際に用いられる。
    例:スキャンした書類から文字起こしを行う、看板の文字を読み取る、など。

  • LLM
    膨大な量のテキストデータをもとに学習された自然言語処理モデルで、人間の言語を理解・生成する能力を持つ。文章生成・要約・翻訳・質問応答など、幅広い言語タスクに応用される。

  • ベクトルデータベース
    テキストや画像、音声などのデータをベクトルとして扱い、そのベクトルデータを保存・検索するためのデータベース

  • ベクトル検索
    データ同士の類似性をもとに検索結果を提供する技術

  • Embedding(埋め込み)
    テキストや画像などのデータを数値ベクトルに変換する技術

  • ChromaDB
    テキストや画像などをベクトル化して保存・管理し、意味的な類似性に基づいて高速検索を可能にする、Chroma社提供のベクトルデータベースです。

実装と結果

1. ライブラリの準備やAPIキーの設定

検証では、LLMを使ったアプリケーションを開発するためのフレームワークであるLangchainを活用します。また、今回の検証で使用するモデルは、OpenAIのGPT-4oとGoogleのGemini2.5 Flashとします。使用した環境はGoogle Colabです。
https://colab.research.google.com/

※GPTやGeminiなどの言語モデルをGoogle Colab上で使用するには、事前に各プロバイダのAPIキーを発行する必要があります。

!pip install langchain_openai
!pip install langchain_google_genai
!pip install langchain_chroma
!pip install python-dotenv
import numpy as np
import pandas as pd
from dotenv import load_dotenv
import os
import sys
import base64
from uuid import uuid4
from PIL import Image

from langchain_openai import ChatOpenAI
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
load_dotenv()

2. さまざまな画像をLLMでOCR

今回の検証では、ホワイトボードの写真だけでなく、レシートやレポートの画像についてもOCRによるテキスト情報の抽出を行いました。全てを掲載すると冗長になるため、本稿では一部の結果のみを抜粋して紹介します。

まずはTodoリストです。

# GPT-4o
def explain_image(filename: str) -> str:
    with open(filename, "rb") as image_file:
      image_data = base64.b64encode(image_file.read()).decode('utf-8')
    llm = ChatOpenAI(model='gpt-4o', temperature=0)

    prompt = """
    ## Role
    あなたは与えられた画像から情報を正しく抽出する、OCRおよび情報抽出の専門エージェントです。

    ## Objective
  与えられた画像からすべてのテキストを正確に抽出してください。

    ## Instructions
    - 画像内に記載されたすべてのテキスト情報を正確に読み取ってください。
    - カンマや句読点なども含まれていれば、見逃さず出力してください。
    - 出力は純粋なテキスト形式で、マークダウンや特殊な書式は使用しないでください。
    - 読み取り項目の出力以外(コメント、説明文、マークダウン、推論過程など)は含めないでください。
    """

    message = HumanMessage(
        content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}},
        ]
    )

    response = llm.invoke([message])
    return response.content

answer1 = explain_image('ToDoリスト.jpg')
print(answer1)

出力は以下の通りです。

6/30〜7/6 ToDo

6/30(月) インターン(作業) 16:00〜24:00
7/1(火) インターン(面談) 13:00〜
7/2(水) 研究相談 9:00〜 M会社説明会 16:00〜19:00
7/3(木) 研究計画書作成、授業の復習・課題
7/4(金) 研究計画書提出締切 12:00まで インターン(作業) 約3時間
7/5(土) インターン(作業) 約3時間、授業の復習・課題
7/6(日) 授業の復習・課題

見た感じ正確に抽出できていそうですが、一点誤りがあります。それはM会社説明会の時間です。画像の方では、16:00~17:00となっていますが、出力だと16:00~19:00になっています。ちなみにGeminiの方だと正しく抽出できていました。

次にブレスト画像です。

# GPT-4o
def explain_image(filename: str) -> str:
    with open(filename, "rb") as image_file:
      image_data = base64.b64encode(image_file.read()).decode('utf-8')
    llm = ChatOpenAI(model='gpt-4o', temperature=0)

    prompt = """
    ## Role
    あなたは与えられた画像から情報を正しく抽出する、OCRおよび情報抽出の専門エージェントです。

    ## Objective
  与えられた画像からすべてのテキストを正確に抽出してください。

    ## Instructions
    - 画像内に記載されたすべてのテキスト情報を正確に読み取ってください。
    - カンマや句読点なども含まれていれば、見逃さず出力してください。
    - 表形式のデータは、その構造を維持してください。各セルの内容を正確に抽出し、セルの区切りには '|' を使用してください。
    - 出力は純粋なテキスト形式で、マークダウンや特殊な書式は使用しないでください。
    - 読み取り項目の出力以外(コメント、説明文、マークダウン、推論過程など)は含めないでください。
    """

    message = HumanMessage(
        content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}},
        ]
    )

    response = llm.invoke([message])
    return response.content

answer3 = explain_image('ブレスト.JPG')
print(answer3)

出力は以下の通りです。

次世代観光アプリのアイデア

【ペルソナ】
- 海外からの旅行者
  => Google Maps 中心に行動、写真映え
- 子連れファミリー
  => ストレス少なく、安全・衛生面重視

【検討アイデア】
- AI によるルート自動生成(複数条件に応じたルート提案)
- 音声ナビゲーション
- その場で翻訳、機能
- 現地住民 × 生活者目線で知らないスポット提案
- 乗り換え自動整理(時刻表 + 位置重視)

【差別化ポイント】

| 機能          | Google Maps | 競合 | 本アプリ |
|---------------|-------------|------|---------|
| ホテル・観光地化 | △           | ◯    | ◎       |
| SNS 連携機能   | ◯           | ◯    | ◎       |
| 感情ベースの提案 | ×           | ◯    | ◎       |

【検討課題】
- 利用シーンの幅 => 日帰り?長期?
- データ収集原点(混雑、気象)
- ユーザーがやってみたいこと、をどう把握

【ネクストプラン】
- ペルソナに応じてユーザーシナリオをマップ化
- リアルタイムのトレンドボード作成(サードパーティデータ)
- データ提供元 API リストアップ

このケースでは、テキストや表の読み取りが上手く行えていないことが一目瞭然です。Geminiの方だと、GPTよりは良好な結果でしたが、それでもいくつかの誤りがありました。

最後にホワイトボード画像ではないですが、参考にレシート画像で試してみます。

# Gemini2.5 Flash
def explain_image(filename: str) -> str:
    with open(filename, "rb") as image_file:
      image_data = base64.b64encode(image_file.read()).decode('utf-8')
    llm = ChatGoogleGenerativeAI(model='gemini-2.5-flash', temperature=0)

    prompt = """
    ## Role
    あなたは与えられた画像から情報を正しく抽出する、OCRおよび情報抽出の専門エージェントです。

    ## Objective
  レシートに記載されている情報を、以下のフィールドに対応する構造化データとして出力してください。

    ## Task
    以下の各項目(フィールド)を抽出してください:
    - store_name(店舗名)
    - transaction_date(取引日時:YYYY/MM/DD HH:MM)
    - items(購入商品リスト:商品名・数量・価格)
    - total(合計金額)

    ## Example
    出力は以下のようなJSON形式で行ってください:
    {
      "store_name": "string",
      "transaction_date": "YYYY/MM/DD HH:MM",
      "items": [
        {
          "name": "string",
          "quantity": "int or string",
          "price": "int"
        }
      ],
      "total": "int"
    }

    ## Instructions
    - 数値は可能な限り半角数字で出力してください。
    - 読み取れない部分は、補完・推測などを行わず、必ず "null" としてください。
    - 出力はJSONのみで、コメントや説明文は不要です。
    """

    message = HumanMessage(
        content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}},
        ]
    )

    response = llm.invoke([message])
    return response.content

answer6 = explain_image('レシート画像1.JPG')
print(answer6)

出力は以下の通りです。

```json
{
  "store_name": "DAISO",
  "transaction_date": "2025/06/24 19:32",
  "items": [
    {
      "name": "ホワイトボードマーカー",
      "quantity": "1",
      "price": 100
    },
    {
      "name": "ホワイトボード(34.5)",
      "quantity": "1",
      "price": 300
    }
  ],
  "total": 440
}

こちらのレシート画像では、正しく情報抽出できています。こちらはGeminiの方を紹介していますが、GPTでも同じような結果となりました。

3. 読み取り結果

GPTやGeminiを用いたOCRの読み取り精度を主観で表にまとめました。

GPT-4o Gemini
Todoリスト画像 本来17でないといけない部分が19になっている 完璧
ブレスト画像 ところどころに誤りがある。表の情報も完全に誤っている。 ほんの少しだけ誤りがある。しかし、表情報は完璧。
レシート1画像 日付以外完璧 日付以外完璧
レシート2画像 ほぼ完璧 ほぼ完璧
レポート画像 テキスト抽出は完璧だが、プロンプトの指示に従っていない。 テキスト抽出完璧で、プロンプトの指示にしっかり従っている。
研究計画画像 完璧 完璧

この結果から、基本的にGPT-4oよりGemini 2.5 Flashの方が、OCR性能が高そうだと感じました。また、手書き文字や内容がごちゃごちゃした画像に関しては、読み取り精度が落ちる傾向にあると感じました。

4. ベクトルデータベースにOCR結果を格納

OpenAIのEmbeddingを用いて、OCRで取得したテキストデータをベクトルデータベースに格納します。ここでは、LangchainのChromaを使用します。

embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
vector_store = Chroma(
    collection_name="example_collection",
    embedding_function=embeddings,
    persist_directory="./chroma_langchain_db",
)
document_1 = Document(
    page_content=answer1,
    metadata={"source": "image", "img_path": "ToDoリスト.jpg"},
    id=1,
)

document_2 = Document(
    page_content=answer2,
    metadata={"source": "image", "img_path": "ToDoリスト.jpg"},
    id=2,
)

document_3 = Document(
    page_content=answer3,
    metadata={"source": "image", "img_path": "ブレスト.JPG"},
    id=3,
)

document_4 = Document(
    page_content=answer4,
    metadata={"source": "image", "img_path": "ブレスト.JPG"},
    id=4,
)

document_5 = Document(
    page_content=answer5,
    metadata={"source": "image", "img_path": "レシート画像1.JPG"},
    id=5,
)

document_6 = Document(
    page_content=answer6,
    metadata={"source": "image", "img_path": "レシート画像1.JPG"},
    id=6,
)

document_7 = Document(
    page_content=answer7,
    metadata={"source": "image", "img_path": "レシート画像4.JPG"},
    id=7,
)

document_8 = Document(
    page_content=answer8,
    metadata={"source": "image", "img_path": "レシート画像4.JPG"},
    id=8,
)

document_9 = Document(
    page_content=answer9,
    metadata={"source": "image", "img_path": "レポート.JPG"},
    id=9,
)

document_10 = Document(
    page_content=answer10,
    metadata={"source": "image", "img_path": "レポート.JPG"},
    id=10,
)

document_11 = Document(
    page_content=answer11,
    metadata={"source": "image", "img_path": "研究テーマ.jpg"},
    id=11,
)

document_12 = Document(
    page_content=answer12,
    metadata={"source": "image", "img_path": "研究テーマ.jpg"},
    id=12,
)
documents = [
    document_1,
    document_2,
    document_3,
    document_4,
    document_5,
    document_6,
    document_7,
    document_8,
    document_9,
    document_10,
    document_11,
    document_12,
]

uuids = [str(uuid4()) for _ in range(len(documents))]
vector_store.add_documents(documents=documents, ids=uuids)

これでベクトルデータベースができました。試しにベクトル検索を行ってみます。

results = vector_store.similarity_search(
    "一週間",
    k=1,
)
for res in results:
    print(f"* {res.page_content} [{res.metadata}]")
* 6/30 ~ 7/6 ToDo
6/30(月) インターン(作業) 16:00~24:00
7/1(火) インターン(面談) 13:00~
7/2(水) 研究相談 9:00~ , M会社説明会 16:00~17:00
7/3(木) 研究計画書作成, 授業の復習・課題
7/4(金) 研究計画書提出締切 12:00まで , インターン(作業) 約3時間
7/5(土) インターン(作業) 約3時間, 授業の復習・課題
7/6(日) 授業の復習・課題 [{'source': 'image', 'img_path': 'ToDoリスト.jpg'}]

上記のように、検索がうまくいっていることがわかります。
最後にベクトル検索によって出力された画像のパスをもとに、画像を出力します。

img = Image.open('ToDoリスト.jpg')
img.thumbnail((800, 800))
img

このような流れで、自身が探している画像に効率よくアクセスできるのではないかと思います。

まとめ

本検証では、ホワイトボードやレシート、レポートなど多様な画像データから、LLMを活用したOCRでテキスト抽出を行い、その結果をChromaDBに格納する一連の流れを実装しました。

従来、会議やディスカッションで生まれた貴重なアイデアや意思決定は、ホワイトボードの写真として保存されるだけで活用されないことが多く、検索や再利用の観点で課題が残っていました。しかし、OCR+Embedding+ベクトルデータベースという仕組みを導入することで、画像に埋もれていた情報も「意味的な類似度検索」ができるナレッジとして活用できることを実証しました。

また、今回の検証で解決したい問題がいくつか生まれました。1つ目は手書きのホワイトボード写真のOCR性能があまり良くなかったことです。これに関しては、画像や文書を区切る仕組みであるレイアウト解析で性能が挙げられるのではないかと考えています。実際にコードは残っていないのですが、一度試したら、今回の記事でも出てきたブレスト画像の読み取り精度が上がりました。また、プロンプトの工夫でも精度を挙げられる可能性はありそうです。2つ目は手書きの数式への対応です。検証の前段階で手書きの数式に対して、LLMを用いたOCRを行ったのですが、読み取り精度はあまりよくありませんでした。これに関しては、数式に特化したOCRツールと、LLMを組み合わせることで読み取り精度があがるのではないかと感じました。また、機会があれば自身が保有している統計学ノートに対してOCRを実施して、ベクトルデータベースにナレッジとして残したいです。

宣伝

弊社ではデータ基盤やLLMのご相談や構築も可能ですので、お気軽にお問合せください。
https://solution.rounda.co.jp/

また、中途採用やインターンなど随時募集中です!

https://www.wantedly.com/companies/company_5576351/projects

Discussion