1. はじめに
今回は、画像化した手書きのメモや領収書をテキスト化し、データとして整理できるシステムの開発方法について紹介します。このシステムを作ろうと思ったきっかけは、先日ある研究者の方と話していた時のことです。
その研究者は、スマホで撮影した手書きのメモや書籍のページ、領収書、請求書、契約書などをPCにたくさん保存しているものの、それらを整理するのが苦手だと言っていました。また、これらのデータをもっとわかりやすくまとめたいとも考えていましたが、画像として保存されているため、テキスト化するのが難しいと悩んでいました。
そこで、私が以前に学んだChatGPTのAPIの使い方を思い出し、これを活用して問題を解決できるのではないかと考えました。この問題を解決するためには、ChatGPTとOCR(光学文字認識)技術を組み合わせることで実現できると判断し、開発を進めました。この記事では、このシステムを作るために調べたことや実際に実装したソースコードを紹介していきます。
2. OCR(光学文字認識)とは
OCR(光学文字認識)技術は、画像に含まれる文字を認識してデジタルデータに変換する技術です。従来から利用されてきましたが、最近再び注目を集めています。その理由は、紙の書類を効率よくデータ化し、業務の効率化や電子文書の検索性向上に役立つからです。
例えば、紙に書かれた文字を人力で入力するのは時間がかかり、ミスも生じやすいため非効率的です。そこでOCRを使えば、スキャンした画像を自動的にテキストデータに変換し、入力作業を大幅に削減できます。また、OCRを使ってデジタル化されたデータは、キーワード検索によって必要な書類を簡単に見つけることができ、業務効率が大きく向上します。
3. Cloud Vision APIの取得方法
今回は、OCR(光学文字認識)技術を活用するために、GoogleのCloud Vision APIを使用して画像からテキストを抽出する方法について解説します。GoogleのCloud Vision APIは、画像内の文字を自動的に認識し、テキストとして抽出する強力なツールです。以下では、APIを取得し、実際に使ってみる手順をわかりやすく説明します。
3.1 APIの取得方法
1.Google Cloud Platform(GCP)にアクセスします
2.新しいプロジェクトを作成します
3.プロジェクト名を付けて、「作成」ボタンをクリックしてください
4.この際必ず今回使用するプロジェクトを選択してください
5.サイドメニューから「APIとサービス」と「有効なAPIとサービス」をクリックしてください
6.「APIとサービスを有効化」をクリックしてください
7.検索フィールドに「cloud vision api」と入力して、Cloud Vision APIをクリックしてください
8.「有効にする」をクリックしてください
9.サイドメニューから「APIとサービス」と「認証情報」をクリックしてください
10.「認証情報を作成」をクリックしてください
11.「API キー」をクリックしてください
12.このようにAPIキーが発行されました。なおキーは絶対に外部公開しないでください。
3.2 Pythonを使って画像からテキストを抽出する
Google Cloud Vision APIを使うための準備が整ったら、Pythonコードを使って画像からテキストを抽出してみましょう。以下は、画像を指定してOCRを実行するサンプルコードです。ちなみにテストに使った画像("C:\Users\user\Desktop\test.png")はこちらです。
次に、画像からテキストを抽出するためのPythonコードを紹介します。APIキーと画像パスを設定し、リクエストを送信してテキストを取得します。
import base64
import requests
import json
# Google Cloud Vision APIキーを入力
API_KEY = "{3.Cloud Vision APIの取得方法で取得したAPI}"
# 解析対象の画像ファイルパスまたはURL
image_path = "C:\\Users\\user\\Desktop\\test.png"
# 画像をBase64形式にエンコード
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
# リクエスト用のJSONデータを作成
request_data = {
"requests": [
{
"image": {"content": encoded_image},
"features": [{"type": "TEXT_DETECTION"}],
}
]
}
# Vision APIのエンドポイントURL
endpoint_url = f"https://vision.googleapis.com/v1/images:annotate?key={API_KEY}"
# APIにPOSTリクエストを送信
response = requests.post(endpoint_url, json=request_data)
# レスポンスを取得
if response.status_code == 200:
response_data = response.json()
# テキスト検出結果を表示
try:
detected_text = response_data["responses"][0]["textAnnotations"][0]["description"]
print("検出されたテキスト:")
print(detected_text)
except (KeyError, IndexError):
print("テキストが検出されませんでした。")
else:
print(f"エラーが発生しました。ステータスコード: {response.status_code}")
print(response.text)
このコードでは、画像を Base64形式 にエンコードしてCloud Vision APIにリクエストを送信しています。Base64 とは、バイナリデータ(この場合は画像ファイル)をテキスト形式に変換するエンコーディング方式です。画像ファイルはバイナリデータとして処理されるため、APIに送信する際にはテキスト形式に変換する必要があります。コード内では、encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
を使って、画像をBase64形式に変換しています。
もしリクエストが成功すれば、以下のようなテキストが表示されます。
検出されたテキスト:
HTMLで空白を入れたい時に使う謎の文字 とは? - あ...
2019/07/29 — 半角スペースを入れたいときには役立つタグ HTMLの文中に を挿入する
と半角スペース分の間隔を空けてくれるという機能を果たしてくれます。
Cloud Vision APIを使用すれば、画像内の文字を簡単に検出できます。今回は、Pythonを使用して実装しましたが、Googleが提供している公式ライブラリを利用すれば、もっと簡潔にコードを書くことができます。ただし、今回は他のプログラミング言語にも応用できるように、あえて公式ライブラリを使わずに、直接HTTPリクエストを送信する方法で実装しました。
また、公式ドキュメントによると、画像からテキストを抽出するには以下のように設定します。
"features": [{"type": "TEXT_DETECTION"}],
この設定では、画像内のテキストを検出して抽出します。
一方、ドキュメント内の密なテキストや手書き文字の検出には、次の設定を使います:
"features": [{"type": "DOCUMENT_TEXT_DETECTION"}],
この設定は、例えば手書き文字や、高密度なテキストのある画像に最適化されています。
3.3 他の言語でもOCRを実行
Python以外の言語でもCloud Vision APIを使うことができます。以下に、C#やVB.NETでの実装例も紹介します。
3.3.1 C#での実装
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace OCR
{
class Program
{
static async Task Main(string[] args)
{
// Google Cloud Vision APIキーを入力
string API_KEY = "{3.Cloud Vision APIの取得方法で取得したAPI}";
// 解析対象の画像ファイルパス
string image_path = @"C:\Users\user\Desktop\test.png";
// 画像をBase64形式にエンコード
string encodedImage = Convert.ToBase64String(File.ReadAllBytes(image_path));
// リクエスト用のJSONデータを作成
var requestData = new
{
requests = new[]
{
new
{
image = new { content = encodedImage },
features = new[]
{
new { type = "TEXT_DETECTION" }
}
}
}
};
// JSONデータを文字列に変換
string jsonRequest = JsonConvert.SerializeObject(requestData);
// Vision APIのエンドポイントURL
string endpointUrl = $"https://vision.googleapis.com/v1/images:annotate?key={API_KEY}";
// HttpClientを使ってPOSTリクエストを送信
using (var client = new HttpClient())
using (var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"))
{
// リクエストを送信
HttpResponseMessage response = await client.PostAsync(endpointUrl, content);
// レスポンスを取得
if (response.IsSuccessStatusCode)
{
string responseBody = await response.Content.ReadAsStringAsync();
// レスポンスのJSONを解析
dynamic responseData = JsonConvert.DeserializeObject(responseBody);
try
{
string detectedText = responseData.responses[0].textAnnotations[0].description;
Console.WriteLine("検出されたテキスト:");
Console.WriteLine(detectedText);
}
catch (Exception ex)
{
Console.WriteLine("テキストが検出されませんでした。");
Console.WriteLine($"エラー: {ex.Message}");
}
}
else
{
Console.WriteLine($"エラーが発生しました。ステータスコード: {response.StatusCode}");
string errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine(errorContent);
}
}
}
}
}
3.3.2 VB.netでの実装
Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Text
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq ' JObjectを使用するためのインポート
Module Program
Sub Main(ByVal args As String())
' Google Cloud Vision APIキーを入力
Dim API_KEY As String = "{3.Cloud Vision APIの取得方法で取得したAPI}"
' 解析対象の画像ファイルパス
Dim image_path As String = "C:\Users\user\Desktop\test.png"
' 画像をBase64形式にエンコード
Dim encodedImage As String = Convert.ToBase64String(File.ReadAllBytes(image_path))
' リクエスト用のJSONデータを作成
Dim requestData = New With {
.requests = New Object() {
New With {
.image = New With {.content = encodedImage},
.features = New Object() {
New With {.type = "TEXT_DETECTION"}
}
}
}
}
' JSONデータを文字列に変換
Dim jsonRequest As String = JsonConvert.SerializeObject(requestData)
' Vision APIのエンドポイントURL
Dim endpointUrl As String = $"https://vision.googleapis.com/v1/images:annotate?key={API_KEY}"
' HttpClientを使ってPOSTリクエストを送信
Using client As New HttpClient()
Using content As New StringContent(jsonRequest, Encoding.UTF8, "application/json")
' リクエストを送信
Dim response As HttpResponseMessage = client.PostAsync(endpointUrl, content).Result
' レスポンスを取得
If response.IsSuccessStatusCode Then
Dim responseBody As String = response.Content.ReadAsStringAsync().Result
' レスポンスのJSONを解析
Dim responseData As JObject = JObject.Parse(responseBody)
Try
' JSON内の「textAnnotations[0].description」を取得
Dim detectedText As String = responseData("responses")(0)("textAnnotations")(0)("description").ToString()
Console.WriteLine("検出されたテキスト:")
Console.WriteLine(detectedText)
Catch ex As Exception
Console.WriteLine("テキストが検出されませんでした。")
Console.WriteLine($"エラー: {ex.Message}")
End Try
Else
Console.WriteLine($"エラーが発生しました。ステータスコード: {response.StatusCode}")
Dim errorContent As String = response.Content.ReadAsStringAsync().Result
Console.WriteLine(errorContent)
End If
End Using
End Using
End Sub
End Module
4. ChatGPT APIの取得方法
今回のプロジェクトでは、OCRに加えて生成AIも活用します。具体的には、生成AIの代表格であるChatGPTを使用します。なお、ChatGPTのAPI取得方法については、私が以前書いた記事に詳しく解説していますので、そちらを参考にしてください。
5. 画像からテキストを抽出し、整理するシステムの実装
この章では、手書きのメモや領収書を画像として取り込み、その内容をテキスト化してデータとして整理するシステムのコードを紹介します。このシステムは、Cloud Vision APIとChatGPT APIを活用して、画像からテキストを抽出し、その情報を分類・整理します。ユーザーが指定したフォルダ内の画像を読み込み、OCR処理を行ってテキストを取り出し、その後、抽出したテキストを領収書、請求書、名刺など、適切な形式に変換します。なお、今回サンプルとして使用した画像は以下のリンクから取得しました。
- 名刺: 通常ライン_名前_シンプル_表面 | 名刺の無料デザインテンプレート | 印刷のラクスル
- 領収書: 領収書エクセルテンプレート シンプル3(無料) - Misoca(ミソカ) - 弥生株式会社【公式】
- 請求書: 請求書エクセルテンプレート(無料)_ヨコ型_品番_004
- メモ:
5.1 Pythonでの実装
import os
import base64
import requests
import json
# Google Cloud Vision APIキー
VISION_API_KEY = "{3.Cloud Vision APIの取得方法で取得したAPI}"
# ChatGPT APIキー
OPENAI_API_KEY = "{4.ChatGPT APIの取得方法で取得したAPI}"
# 解析対象のディレクトリパス
DIRECTORY_PATH = "C:\\Users\\user\\Desktop\\image"
# Vision APIエンドポイントURL
VISION_API_URL = f"https://vision.googleapis.com/v1/images:annotate?key={VISION_API_KEY}"
# ChatGPT APIエンドポイントURL
OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
# サポートされる画像ファイル拡張子
SUPPORTED_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".gif")
def encode_image_to_base64(file_path):
with open(file_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def call_vision_api(encoded_image):
# APIリクエスト用データ
request_data = {
"requests": [
{
"image": {"content": encoded_image},
"features": [{"type": "TEXT_DETECTION"}],
}
]
}
# APIリクエストを送信
response = requests.post(VISION_API_URL, json=request_data)
# レスポンスの解析
if response.status_code == 200: # 成功した場合
try:
# テキスト検出結果を取得
response_data = response.json()
return response_data["responses"][0]["textAnnotations"][0]["description"]
except (KeyError, IndexError):
# 検出結果が空の場合
return "テキストが検出されませんでした。"
else:
# エラー発生時のメッセージ
return f"エラー: {response.status_code} - {response.text}"
def format_prompt_for_openai(detected_text):
return f"""# OCRデータの形式判別と変換指示
## OCRデータ
""" + detected_text + """
---
**まず、上記のOCRデータの内容を解析し、以下のどれかの形式に分類してください**:
- **領収書 (Receipt)**
- **請求書 (Invoice)**
- **手書きのメモ (Handwritten Memo)**
- **名刺情報 (Business Card)**
- **契約書 (Contract)**
- **その他 (Other)**
---
### **領収書 (Receipt) の場合**
- OCRデータが領収書に該当する場合、その内容を以下の領収書フォーマットに変換してください。
```json
{
"type": "receipt",
"receipt_id": "R-20241123-001",
"date": "2024-11-23",
"issued_by": "ABC商店",
"received_by": "山田 太郎",
"amount": {
"currency": "JPY",
"value": 1500
},
"payment_method": "現金",
"details": [
{
"item_name": "商品A",
"quantity": 1,
"price_per_unit": 1000,
"total_price": 1000
},
{
"item_name": "商品B",
"quantity": 1,
"price_per_unit": 500,
"total_price": 500
}
],
"remarks": "ご利用ありがとうございました。"
}
\```
- 必要な情報: 日付、発行元、受領者、金額、支払い方法、品目詳細(商品名、数量、単価、金額)、備考
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
\---
### **請求書 (Invoice) の場合**
- OCRデータが請求書に該当する場合、その内容を以下の請求書フォーマットに変換してください。
\```json
{
"type": "invoice",
"invoice_id": "INV-20241123-001",
"date": "2024-11-23",
"due_date": "2024-12-15",
"issued_by": {
"company_name": "XYZ株式会社",
"address": "東京都港区1-1-1",
"contact": "03-1234-5678",
"email": "info@xyz.co.jp"
},
"billed_to": {
"company_name": "山田商事",
"address": "大阪府大阪市2-2-2",
"contact": "06-9876-5432",
"email": "yamada@business.co.jp"
},
"items": [
{
"item_name": "サービスA",
"description": "サービス内容の詳細説明",
"quantity": 1,
"price_per_unit": 100000,
"total_price": 100000
},
{
"item_name": "商品B",
"description": "商品の説明",
"quantity": 5,
"price_per_unit": 20000,
"total_price": 100000
}
],
"subtotal": 200000,
"tax": {
"rate": 0.1,
"amount": 20000
},
"total": 220000,
"payment_instructions": "振込先: 三菱UFJ銀行 東京支店 普通口座 1234567"
}
\```
- 必要な情報: 請求書番号、請求日、支払期限、発行者情報(会社名、住所、連絡先、メール)、請求先情報、品目詳細(商品名、説明、数量、単価、金額)、小計、消費税、合計金額、振込先情報
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
\---
### **手書きメモ (Handwritten Memo) の場合**
OCRデータが手書きメモに該当する場合、その内容を以下の手書きメモフォーマットに変換してください。
\```json
{
"type": "handwritten_memo",
"memo_id": "M-20241123-001",
"date": "2024-11-23",
"author": "山田 太郎",
"content": "来週の会議は火曜日の午前10時から。\n場所: 会議室A。\n議題: 新商品開発について。",
"tags": ["会議", "予定", "重要"],
"priority": "高",
"related_documents": ["INV-20241123-001", "R-20241123-001"]
}
\```
- 必要な情報: メモの日時、作成者、内容、タグ、優先度、関連する書類
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **名刺情報 (Business Card) の場合**
- OCRデータが名刺情報に該当する場合、その内容を以下の名刺フォーマットに変換してください。
\```json
{
"type": "business_card",
"business_card_id": "BC-20241123-001",
"name": "田中 一郎",
"job_title": "営業部長",
"company_name": "株式会社サンプル",
"address": "東京都渋谷区1-2-3",
"phone": "+81-3-1234-5678",
"email": "ichiro.tanaka@sample.co.jp",
"website": "https://www.sample.co.jp",
"social_media": {
"linkedin": "https://www.linkedin.com/in/ichiro-tanaka",
"twitter": "https://twitter.com/ichiro_tanaka"
}
}
\```
- 必要な情報: 名前、役職、会社名、住所、電話番号、メールアドレス、ウェブサイト、ソーシャルメディアリンク
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
### **契約書 (Contract) の場合**
- OCRデータが契約書に該当する場合、その内容を以下の契約書フォーマットに変換してください。
```json
{
"type": "contract",
"contract_id": "CON-20241123-001",
"contract_date": "2024-11-23",
"parties": [
{
"party_name": "株式会社サンプル",
"role": "甲",
"address": "東京都渋谷区1-2-3"
},
{
"party_name": "株式会社テスト",
"role": "乙",
"address": "大阪府大阪市4-5-6"
}
],
"terms": [
{
"description": "契約期間",
"details": "2024年12月1日から2025年11月30日まで"
},
{
"description": "支払条件",
"details": "毎月末に指定の口座に振り込み"
}
],
"signatures": [
{
"party_name": "株式会社サンプル",
"signatory_name": "山田 太郎",
"sign_date": "2024-11-23"
},
{
"party_name": "株式会社テスト",
"signatory_name": "佐藤 花子",
"sign_date": "2024-11-23"
}
]
}
\```
- 必要な情報: 契約書番号、契約日、契約当事者(甲、乙)、契約内容(契約期間、支払条件)、署名者情報(署名日含む)
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
\---
### **その他 (Other) の場合**
OCRデータが上記のいずれにも該当しない場合、その内容を「その他」フォーマットに変換してください。
```json
{
"type": "other",
"content": "ここにOCRから抽出されたテキスト情報が入ります。"
}
\```
- 不明な部分については、そのままテキスト情報を「content」フィールドに記載してください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
"""
def call_openai_api(prompt):
# リクエストボディ
request_body = {
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
],
"temperature": 0 # 応答の一貫性を保つため低温度を設定
}
# HTTPヘッダー
headers = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
}
# APIリクエストを送信
response = requests.post(OPENAI_API_URL, headers=headers, data=json.dumps(request_body))
# レスポンスの解析
if response.status_code == 200: # 成功した場合
response_data = response.json()
return response_data['choices'][0]['message']['content']
else:
# エラー発生時のメッセージ
return f"Error: {response.status_code}\n{response.text}"
def process_images_in_directory(directory_path):
# ディレクトリ内の全ファイルを走査
for root, _, files in os.walk(directory_path):
for file in files:
# サポートされる拡張子のファイルのみを対象
if file.lower().endswith(SUPPORTED_EXTENSIONS):
file_path = os.path.join(root, file)
print(f"\n処理中のファイル: {file_path}")
# 画像をBase64エンコード
encoded_image = encode_image_to_base64(file_path)
# OCR処理
detected_text = call_vision_api(encoded_image)
print(f"\n検出されたテキスト:\n{detected_text}")
# ChatGPT API用プロンプトを作成
prompt = format_prompt_for_openai(detected_text)
# ChatGPT APIを呼び出し、結果を取得
formatted_data = call_openai_api(prompt)
print(f"\n抽出されたデータ:\n{formatted_data}")
print("-------------------")
if __name__ == "__main__":
# メイン処理: 指定ディレクトリ内の画像を処理
process_images_in_directory(DIRECTORY_PATH)
もしリクエストが成功すれば、以下のようなテキストが表示されます。
処理中のファイル: C:\Users\user\Desktop\image\memo.png
検出されたテキスト:
メモ
山田さんからお電話です
抽出されたデータ:
\```json
{
"type": "handwritten_memo",
"memo_id": null,
"date": null,
"author": null,
"content": "メモ\n山田さんからお電話です",
"tags": null,
"priority": null,
"related_documents": null
}
\```
-------------------
処理中のファイル: C:\Users\user\Desktop\image\simple-03.png
検出されたテキスト:
○○株式会社
○○○○ 御中
内訳
税別金額
消費稅額
領収書
¥3,300-
金額
但し
上記正に領収いたしました。
No.
発行日
200
株式会社○○○
印收
紙入
〒000-0000
愛知県名古屋市中村区名駅2丁目35番22号
メビウス名古屋ビル2階
TEL:
FAX:
抽出されたデータ:
```json
{
"type": "receipt",
"receipt_id": null,
"date": null,
"issued_by": "株式会社○○○",
"received_by": "○○○○",
"amount": {
"currency": "JPY",
"value": 3300
},
"payment_method": null,
"details": [],
"remarks": "上記正に領収いたしました。"
}
\```
-------------------
・・・・
5.2 他の言語でもOCRを実行
Python以外の言語でも上記の機能を実装することができます。以下に、C#やVB.NETでの実装例も紹介します。
5.2.1 C#での実装
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
class OCR
{
// APIキーとエンドポイント
private static string VISION_API_KEY = "{3.Cloud Vision APIの取得方法で取得したAPI}";
private static string OPENAI_API_KEY = "{4.ChatGPT APIの取得方法で取得したAPI}";
private static string DIRECTORY_PATH = @"C:\Users\user\Desktop\image";
// Vision APIエンドポイントURL
private static string VISION_API_URL = $"https://vision.googleapis.com/v1/images:annotate?key={VISION_API_KEY}";
// ChatGPT APIエンドポイントURL
private static string OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
// サポートされる画像ファイル拡張子
private static readonly string[] SUPPORTED_EXTENSIONS = { ".png", ".jpg", ".jpeg", ".bmp", ".gif" };
public static async Task Main(string[] args)
{
await ProcessImagesInDirectory(DIRECTORY_PATH);
}
public static async Task ProcessImagesInDirectory(string directoryPath)
{
foreach (var filePath in Directory.GetFiles(directoryPath))
{
if (Array.Exists(SUPPORTED_EXTENSIONS, ext => filePath.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
{
Console.WriteLine($"\n処理中のファイル: {filePath}");
// 画像をBase64エンコード
string encodedImage = EncodeImageToBase64(filePath);
// OCR処理
string detectedText = await CallVisionApi(encodedImage);
Console.WriteLine($"\n抽出されたデータ:\n{detectedText}");
// ChatGPT API用プロンプトを作成
string prompt = FormatPromptForChatGPT(detectedText);
// ChatGPT APIを呼び出し、結果を取得
string formattedData = await CallOpenAiApi(prompt);
Console.WriteLine($"\n検出されたテキスト:\n{formattedData}");
Console.WriteLine("-------------------");
}
}
}
public static string EncodeImageToBase64(string filePath)
{
byte[] imageBytes = File.ReadAllBytes(filePath);
return Convert.ToBase64String(imageBytes);
}
public static async Task<string> CallVisionApi(string encodedImage)
{
var requestData = new
{
requests = new[]
{
new
{
image = new { content = encodedImage },
features = new[] { new { type = "TEXT_DETECTION" } }
}
}
};
var client = new HttpClient();
var content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.PostAsync(VISION_API_URL, content);
string responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
dynamic jsonResponse = JsonConvert.DeserializeObject(responseContent);
try
{
return jsonResponse.responses[0].textAnnotations[0].description;
}
catch
{
return "テキストが検出されませんでした。";
}
}
else
{
return $"Error: {response.StatusCode} - {responseContent}";
}
}
public static string FormatPromptForChatGPT(string detectedText)
{
return $@"
# OCRデータの形式判別と変換指示
## OCRデータ
{detectedText}
---
**まず、上記のOCRデータの内容を解析し、以下のどれかの形式に分類してください**:
- **領収書 (Receipt)**
- **請求書 (Invoice)**
- **手書きのメモ (Handwritten Memo)**
- **名刺情報 (Business Card)**
- **契約書 (Contract)**
- **その他 (Other)**
---
### **領収書 (Receipt) の場合**
- OCRデータが領収書に該当する場合、その内容を以下の領収書フォーマットに変換してください。
json
{{
""type"": ""receipt"",
""receipt_id"": ""R-20241123-001"",
""date"": ""2024-11-23"",
""issued_by"": ""ABC商店"",
""received_by"": ""山田 太郎"",
""amount"": {{
""currency"": ""JPY"",
""value"": 1500
}},
""payment_method"": ""現金"",
""details"": [
{{
""item_name"": ""商品A"",
""quantity"": 1,
""price_per_unit"": 1000,
""total_price"": 1000
}},
{{
""item_name"": ""商品B"",
""quantity"": 1,
""price_per_unit"": 500,
""total_price"": 500
}}
],
""remarks"": ""ご利用ありがとうございました。""
}}
- 必要な情報: 日付、発行元、受領者、金額、支払い方法、品目詳細(商品名、数量、単価、金額)、備考
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **請求書 (Invoice) の場合**
- OCRデータが請求書に該当する場合、その内容を以下の請求書フォーマットに変換してください。
json
{{
""type"": ""invoice"",
""invoice_id"": ""INV-20241123-001"",
""date"": ""2024-11-23"",
""due_date"": ""2024-12-15"",
""issued_by"": {{
""company_name"": ""XYZ株式会社"",
""address"": ""東京都港区1-1-1"",
""contact"": ""03-1234-5678"",
""email"": ""info@xyz.co.jp""
}},
""billed_to"": {{
""company_name"": ""山田商事"",
""address"": ""大阪府大阪市2-2-2"",
""contact"": ""06-9876-5432"",
""email"": ""yamada@business.co.jp""
}},
""items"": [
{{
""item_name"": ""サービスA"",
""description"": ""サービス内容の詳細説明"",
""quantity"": 1,
""price_per_unit"": 100000,
""total_price"": 100000
}},
{{
""item_name"": ""商品B"",
""description"": ""商品の説明"",
""quantity"": 5,
""price_per_unit"": 20000,
""total_price"": 100000
}}
],
""subtotal"": 200000,
""tax"": {{
""rate"": 0.1,
""amount"": 20000
}},
""total"": 220000,
""payment_instructions"": ""振込先: 三菱UFJ銀行 東京支店 普通口座 1234567""
}}
- 必要な情報: 請求書番号、請求日、支払期限、発行者情報(会社名、住所、連絡先、メール)、請求先情報、品目詳細(商品名、説明、数量、単価、金額)、小計、消費税、合計金額、振込先情報
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **手書きメモ (Handwritten Memo) の場合**
- OCRデータが手書きメモに該当する場合、その内容を以下の手書きメモフォーマットに変換してください。
json
{{
""type"": ""handwritten_memo"",
""memo_id"": ""M-20241123-001"",
""date"": ""2024-11-23"",
""author"": ""山田 太郎"",
""content"": ""来週の会議は火曜日の午前10時から。\n場所: 会議室A。\n議題: 新商品開発について。"",
""tags"": [""会議"", ""予定"", ""重要""],
""priority"": ""高"",
""related_documents"": [""INV-20241123-001"", ""R-20241123-001""]
}}
- 必要な情報: メモの日時、作成者、内容、タグ、優先度、関連する書類
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **名刺情報 (Business Card) の場合**
- OCRデータが名刺情報に該当する場合、その内容を以下の名刺フォーマットに変換してください。
json
{{
""type"": ""business_card"",
""business_card_id"": ""BC-20241123-001"",
""name"": ""田中 一郎"",
""job_title"": ""営業部長"",
""company_name"": ""株式会社サンプル"",
""address"": ""東京都渋谷区1-2-3"",
""phone"": ""+81-3-1234-5678"",
""email"": ""ichiro.tanaka@sample.co.jp"",
""website"": ""https://www.sample.co.jp"",
""social_media"": {{
""linkedin"": ""https://www.linkedin.com/in/ichiro-tanaka"",
""twitter"": ""https://twitter.com/ichiro_tanaka""
}}
}}
- 必要な情報: 名前、役職、会社名、住所、電話番号、メールアドレス、ウェブサイト、ソーシャルメディアリンク
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **契約書 (Contract) の場合**
- OCRデータが契約書に該当する場合、その内容を以下の契約書フォーマットに変換してください。
json
{{
""type"": ""contract"",
""contract_id"": ""CON-20241123-001"",
""contract_date"": ""2024-11-23"",
""parties"": [
{{
""party_name"": ""株式会社サンプル"",
""role"": ""甲"",
""address"": ""東京都渋谷区1-2-3""
}},
{{
""party_name"": ""株式会社テスト"",
""role"": ""乙"",
""address"": ""大阪府大阪市4-5-6""
}}
],
""terms"": [
{{
""description"": ""契約期間"",
""details"": ""2024年12月1日から2025年11月30日まで""
}},
{{
""description"": ""支払条件"",
""details"": ""毎月末に指定の口座に振り込み""
}}
],
""signatures"": [
{{
""party_name"": ""株式会社サンプル"",
""signatory_name"": ""山田 太郎"",
""sign_date"": ""2024-11-23""
}},
{{
""party_name"": ""株式会社テスト"",
""signatory_name"": ""佐藤 花子"",
""sign_date"": ""2024-11-23""
}}
]
}}
- 必要な情報: 契約書番号、契約日、契約当事者(甲、乙)、契約内容(契約期間、支払条件)、署名者情報(署名日含む)
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **その他 (Other) の場合**
- OCRデータが上記のいずれにも該当しない場合、その内容を「その他」フォーマットに変換してください。
json
{{
""type"": ""other"",
""content"": ""ここにOCRから抽出されたテキスト情報が入ります。""
}}
- 不明な部分については、そのままテキスト情報を「content」フィールドに記載してください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。";
}
public static async Task<string> CallOpenAiApi(string prompt)
{
var requestBody = new
{
model = "gpt-4o",
messages = new[]
{
new { role = "system", content = "You are a helpful assistant." },
new { role = "user", content = prompt }
},
temperature = 0
};
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {OPENAI_API_KEY}");
var content = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json");
var response = await client.PostAsync(OPENAI_API_URL, content);
string responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
dynamic jsonResponse = JsonConvert.DeserializeObject(responseContent);
return jsonResponse.choices[0].message.content;
}
else
{
return $"Error: {response.StatusCode} - {responseContent}";
}
}
}
5.2.2 VB.netでの実装
Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Text
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq ' JObjectを使用するためのインポート
Module Program
' APIキーとエンドポイントURL
Private VISION_API_KEY As String = "{3.Cloud Vision APIの取得方法で取得したAPI}"
Private OPENAI_API_KEY As String = "{4.ChatGPT APIの取得方法で取得したAPI}"
Private DIRECTORY_PATH As String = "C:\Users\user\Desktop\image"
' Vision APIエンドポイントURL
Private VISION_API_URL As String = $"https://vision.googleapis.com/v1/images:annotate?key={VISION_API_KEY}"
' ChatGPT APIエンドポイントURL
Private OPENAI_API_URL As String = "https://api.openai.com/v1/chat/completions"
' サポートされる画像ファイル拡張子
Private SUPPORTED_EXTENSIONS As String() = {".png", ".jpg", ".jpeg", ".bmp", ".gif"}
Sub Main()
ProcessImagesInDirectory(DIRECTORY_PATH).Wait()
End Sub
' 画像フォルダ内の画像を処理
Private Async Function ProcessImagesInDirectory(directoryPath As String) As Task
For Each filePath In Directory.GetFiles(directoryPath)
If SUPPORTED_EXTENSIONS.Any(Function(ext) filePath.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) Then
Console.WriteLine($"処理中のファイル: {filePath}")
' 画像をBase64エンコード
Dim encodedImage As String = EncodeImageToBase64(filePath)
' OCR処理
Dim detectedText As String = Await CallVisionApi(encodedImage)
Console.WriteLine($"\n抽出されたデータ:\n{detectedText}")
' ChatGPT API用プロンプトを作成
Dim prompt As String = FormatPromptForChatGPT(detectedText)
' ChatGPT APIを呼び出し、結果を取得
Dim formattedData As String = Await CallOpenAiApi(prompt)
Console.WriteLine($"\n検出されたテキスト:\n{formattedData}")
Console.WriteLine("-------------------")
End If
Next
End Function
' 画像ファイルをBase64エンコードするメソッド
Private Function EncodeImageToBase64(filePath As String) As String
Dim imageBytes As Byte() = File.ReadAllBytes(filePath)
Return Convert.ToBase64String(imageBytes)
End Function
' Google Vision APIを呼び出してOCRを実行するメソッド
Private Async Function CallVisionApi(encodedImage As String) As Task(Of String)
Dim requestData = New With {
.requests = New Object() {
New With {
.image = New With {.content = encodedImage},
.features = New Object() {
New With {.type = "TEXT_DETECTION"}
}
}
}
}
Dim jsonRequest As String = JsonConvert.SerializeObject(requestData)
Using client As New HttpClient()
Using content As New StringContent(jsonRequest, Encoding.UTF8, "application/json")
' Vision APIへのリクエスト送信
Dim response As HttpResponseMessage = Await client.PostAsync(VISION_API_URL, content)
Dim responseContent As String = Await response.Content.ReadAsStringAsync()
If response.IsSuccessStatusCode Then
' レスポンスを解析
Dim responseData As JObject = JObject.Parse(responseContent)
Try
' OCRで検出されたテキストを取得
Return responseData("responses")(0)("textAnnotations")(0)("description").ToString()
Catch ex As Exception
Return "テキストが検出されませんでした。"
End Try
Else
Return $"Error: {response.StatusCode} - {responseContent}"
End If
End Using
End Using
End Function
' ChatGPT API用のプロンプトを作成するメソッド
Private Function FormatPromptForChatGPT(detectedText As String) As String
Return $"
# OCRデータの形式判別と変換指示
## OCRデータ
{detectedText}
---
**まず、上記のOCRデータの内容を解析し、以下のどれかの形式に分類してください**:
- **領収書 (Receipt)**
- **請求書 (Invoice)**
- **手書きのメモ (Handwritten Memo)**
- **名刺情報 (Business Card)**
- **契約書 (Contract)**
- **その他 (Other)**
---
### **領収書 (Receipt) の場合**
- OCRデータが領収書に該当する場合、その内容を以下の領収書フォーマットに変換してください。
json
{{
""type"": ""receipt"",
""receipt_id"": ""R-20241123-001"",
""date"": ""2024-11-23"",
""issued_by"": ""ABC商店"",
""received_by"": ""山田 太郎"",
""amount"": {{
""currency"": ""JPY"",
""value"": 1500
}},
""payment_method"": ""現金"",
""details"": [
{{
""item_name"": ""商品A"",
""quantity"": 1,
""price_per_unit"": 1000,
""total_price"": 1000
}},
{{
""item_name"": ""商品B"",
""quantity"": 1,
""price_per_unit"": 500,
""total_price"": 500
}}
],
""remarks"": ""ご利用ありがとうございました。""
}}
- 必要な情報: 日付、発行元、受領者、金額、支払い方法、品目詳細(商品名、数量、単価、金額)、備考
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **請求書 (Invoice) の場合**
- OCRデータが請求書に該当する場合、その内容を以下の請求書フォーマットに変換してください。
json
{{
""type"": ""invoice"",
""invoice_id"": ""INV-20241123-001"",
""date"": ""2024-11-23"",
""due_date"": ""2024-12-15"",
""issued_by"": {{
""company_name"": ""XYZ株式会社"",
""address"": ""東京都港区1-1-1"",
""contact"": ""03-1234-5678"",
""email"": ""info@xyz.co.jp""
}},
""billed_to"": {{
""company_name"": ""山田商事"",
""address"": ""大阪府大阪市2-2-2"",
""contact"": ""06-9876-5432"",
""email"": ""yamada@business.co.jp""
}},
""items"": [
{{
""item_name"": ""サービスA"",
""description"": ""サービス内容の詳細説明"",
""quantity"": 1,
""price_per_unit"": 100000,
""total_price"": 100000
}},
{{
""item_name"": ""商品B"",
""description"": ""商品の説明"",
""quantity"": 5,
""price_per_unit"": 20000,
""total_price"": 100000
}}
],
""subtotal"": 200000,
""tax"": {{
""rate"": 0.1,
""amount"": 20000
}},
""total"": 220000,
""payment_instructions"": ""振込先: 三菱UFJ銀行 東京支店 普通口座 1234567""
}}
- 必要な情報: 請求書番号、請求日、支払期限、発行者情報(会社名、住所、連絡先、メール)、請求先情報、品目詳細(商品名、説明、数量、単価、金額)、小計、消費税、合計金額、振込先情報
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **手書きメモ (Handwritten Memo) の場合**
- OCRデータが手書きメモに該当する場合、その内容を以下の手書きメモフォーマットに変換してください。
json
{{
""type"": ""handwritten_memo"",
""memo_id"": ""M-20241123-001"",
""date"": ""2024-11-23"",
""author"": ""山田 太郎"",
""content"": ""来週の会議は火曜日の午前10時から。\n場所: 会議室A。\n議題: 新商品開発について。"",
""tags"": [""会議"", ""予定"", ""重要""],
""priority"": ""高"",
""related_documents"": [""INV-20241123-001"", ""R-20241123-001""]
}}
- 必要な情報: メモの日時、作成者、内容、タグ、優先度、関連する書類
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **名刺情報 (Business Card) の場合**
- OCRデータが名刺情報に該当する場合、その内容を以下の名刺フォーマットに変換してください。
json
{{
""type"": ""business_card"",
""business_card_id"": ""BC-20241123-001"",
""name"": ""田中 一郎"",
""job_title"": ""営業部長"",
""company_name"": ""株式会社サンプル"",
""address"": ""東京都渋谷区1-2-3"",
""phone"": ""+81-3-1234-5678"",
""email"": ""ichiro.tanaka@sample.co.jp"",
""website"": ""https://www.sample.co.jp"",
""social_media"": {{
""linkedin"": ""https://www.linkedin.com/in/ichiro-tanaka"",
""twitter"": ""https://twitter.com/ichiro_tanaka""
}}
}}
- 必要な情報: 名前、役職、会社名、住所、電話番号、メールアドレス、ウェブサイト、ソーシャルメディアリンク
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **契約書 (Contract) の場合**
- OCRデータが契約書に該当する場合、その内容を以下の契約書フォーマットに変換してください。
json
{{
""type"": ""contract"",
""contract_id"": ""CON-20241123-001"",
""contract_date"": ""2024-11-23"",
""parties"": [
{{
""party_name"": ""株式会社サンプル"",
""role"": ""甲"",
""address"": ""東京都渋谷区1-2-3""
}},
{{
""party_name"": ""株式会社テスト"",
""role"": ""乙"",
""address"": ""大阪府大阪市4-5-6""
}}
],
""terms"": [
{{
""description"": ""契約期間"",
""details"": ""2024年12月1日から2025年11月30日まで""
}},
{{
""description"": ""支払条件"",
""details"": ""毎月末に指定の口座に振り込み""
}}
],
""signatures"": [
{{
""party_name"": ""株式会社サンプル"",
""signatory_name"": ""山田 太郎"",
""sign_date"": ""2024-11-23""
}},
{{
""party_name"": ""株式会社テスト"",
""signatory_name"": ""佐藤 花子"",
""sign_date"": ""2024-11-23""
}}
]
}}
- 必要な情報: 契約書番号、契約日、契約当事者(甲、乙)、契約内容(契約期間、支払条件)、署名者情報(署名日含む)
- 不明な部分はNULLにしてください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。
---
### **その他 (Other) の場合**
- OCRデータが上記のいずれにも該当しない場合、その内容を「その他」フォーマットに変換してください。
json
{{
""type"": ""other"",
""content"": ""ここにOCRから抽出されたテキスト情報が入ります。""
}}
- 不明な部分については、そのままテキスト情報を「content」フィールドに記載してください。
- 出力するのはJSONのみでお願いします。それ以外の文言は出力しないでください。"
End Function
' ChatGPT APIを呼び出して結果を取得するメソッド
Private Async Function CallOpenAiApi(prompt As String) As Task(Of String)
Dim requestBody = New With {
.model = "gpt-4o",
.messages = New Object() {
New With {.role = "system", .content = "You are a helpful assistant."},
New With {.role = "user", .content = prompt}
},
.temperature = 0
}
Dim jsonRequest As String = JsonConvert.SerializeObject(requestBody)
Using client As New HttpClient()
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {OPENAI_API_KEY}")
Using content As New StringContent(jsonRequest, Encoding.UTF8, "application/json")
' ChatGPT APIへのリクエスト送信
Dim response As HttpResponseMessage = Await client.PostAsync(OPENAI_API_URL, content)
Dim responseContent As String = Await response.Content.ReadAsStringAsync()
If response.IsSuccessStatusCode Then
' レスポンスからメッセージを取得
Dim jsonResponse As JObject = JObject.Parse(responseContent)
Return jsonResponse("choices")(0)("message")("content").ToString()
Else
Return $"Error: {response.StatusCode} - {responseContent}"
End If
End Using
End Using
End Function
End Module
5.2 機能の説明
このスクリプトは、指定したフォルダ内の画像を読み込み、Cloud Vision APIのOCR機能を使ってテキストを抽出し、その後、ChatGPT APIを利用して抽出されたテキストを分類・形式化します。
5.2.1 手順
- 画像ファイルをBase64形式に変換します。これにより、画像をAPIリクエストで送信できる形式にします。
- エンコードされた画像をCloud Vision APIに送信して、画像内のテキストを検出します。
- 検出されたテキストを元に、ChatGPT APIに送信するためのプロンプトを作成します。このプロンプトには、テキストの分類指示や形式化の指示が含まれています。
- 作成したプロンプトをChatGPT APIに送信し、テキストをJSON形式で整形された結果を取得します。
- 指定されたディレクトリ内のすべての画像について上記の手順を繰り返します。
5.2.2 まとめ
要するに、このコードはCloud Vision APIとChatGPT APIがなければ機能しません。他人のAPIに頼る形になっています。本当はOCRや生成AIの機能を自分でゼロから作りたいのですが、今の私の能力では難しいです。それよりも、既に性能の良いAPIを利用する方が効率的だと考え、このコードを実装しました笑
まとめ
今回は、Cloud Vision APIを使って、画像化された手書きメモや領収書をテキスト化し、整理するシステムの作り方を解説しました。初心者の方にも分かりやすくお伝えしたつもりですが、いかがだったでしょうか?
実は、この記事で紹介したような機能を持つ製品は既に多く市販されています。私もいくつか試したことがありますが、どれも非常に便利で簡単に利用できました。しかし、私はこういったサービスを利用する際には慎重な姿勢を取っています。その理由を以下にまとめました。
他社サービスを利用する際の課題
-
コストがかかる
サブスクリプションなどで毎月料金が発生するため、長期的には意外と高額になります。 -
サービス停止のリスク
提供元の都合でサービスが突然終了してしまうと、使えなくなる恐れがあります。 -
複数サービスの管理が大変
複数のサービスを利用すると、管理が煩雑になりがちです。 -
カスタマイズの限界
データベースや他のシステムとの連携が難しく、ニーズに合ったカスタマイズができない場合があります。
自作する場合のメリット
-
コストを抑えられる
主にAPIの利用料金だけで済むため、結果的に安くなることが多いです。 -
安定して使える
使用しているAPIが提供され続ける限り、サービス停止のリスクがほぼありません。 -
自由なカスタマイズ
ゼロから構築するため、操作性や他のシステムとの連携を自由に調整できます。
もちろん、他社のサービスを利用するか、自作するかは人それぞれの判断です。 製品の利便性を重視するのも良いですし、自分に合った仕組みを構築する楽しさを選ぶのも一つの方法です。どちらが良いか迷っている方は、ぜひこの記事を参考にして、自分に合った方法を見つけてみてください!
それでは、また。
参考文献
OCRとは~業務効率ツールとして注目| リコー
Google Cloud Vision APIのOCRを使ってPythonから文字認識する方法 | Valmore
Cloud Vision API に対する認証 | Vision API Product Search | Google Cloud
画像内のテキストを検出する | Cloud Vision API | Google Cloud
画像内の手書き文字を検出する | Cloud Vision API | Google Cloud