RustでのDocumentAI(OCR用のAIサービス)の使い方
こんにちは。PharmaXエンジニアリング責任者の上野(@ueeeeniki)です。
PharmaXでは、最近Rustの採用を一部のプロダクトからを始めています。
前回の記事『RustでのAzure OpenAI ServiceのAPIの使い方』でも説明したように、プロダクトの中でAzure OpenAI ServiceでのGPT-4の活用を行うなど、積極的にLLMの実戦投入を行っています。
PharmaXは、オンライン薬局を運営するスタートアップであり、LLMの活用対象となるのは医療情報です。
例えば、処方せんの情報などが活用対象に当たりますが、処方せんはPDFや画像などの形式で保存されているテキスト情報であり、LLMにテキストを与える前にOCRなどでテキストを抽出する必要があります。
処方せんをどのように活用するのかについては、『[LLM PoC]Whisper + GPT-4で服薬指導の音声から薬歴内容を自動生成させることに成功しました』や『[LLM PoC]LLMによる疑義照会の半自動化PoC』をご覧ください。
ここでは、GCPが提供するOCRサービスの1つである、Document AIを使ってみます。
公式ドキュメントに複数の言語での使い方は記載されているものの、当然のようにRustでの使い方は載っていないので、今回もRustでDocument AIを使ってみるというニッチな記事を書いてみたいと思います。
Rustを使わない方にとっても参考になる記事にはなっていると思うので、ぜひお読みいただければうれしいです。
Document AIとは?
前述のようにDocument AIは、GCPが提供するOCRのAIサービスです。
PharmaXでは、主要アプリケーションのインフラにGCPを利用しているので、OCRもGCPのサービスから選定をしたいと考えました。
Document AIは、汎用的なOCRを提供するだけではなく、請求書や給与明細、運転免許証などのOCRニーズの高そうなドキュメント形式向けにトレーニングされたモデルも提供されています。
(日本のフォーマットにどの程度対応できているのかは分かりません)
公式ドキュメントをご覧いただければ分かるようにデモ環境も与えられています。
実際にデモ環境に冒頭で引用した処方せんサンプルを入れてみると、OCRで読み取ったテキストをただ返してくれるだけではなく、画像内のテキストの塊ごとに分割してくれることも分かります。
このように何かと使い勝手がよさそうなDocument AIですが、非常にややこしいことに、Googleが提供するOCRのAIには、Document AI以外にも、Vision AIやGoogle Drive APIなどがあります。
どれを使えばいいのかが、少し調べただけでは非常に分かりづらいのが難点です。
このように利用用途の被ったサービスが乱立してどれを使えばいいのかがよく分からなくなってしまうのは、クラウドサービスの常です。
そこで、どのAIサービスを使うべきなのかをChatGPTに聞いていみたいと思います。
ChatGPTの答えの引用だけでかなり長くなってしまいましたが、今回の場合はGoogle Driveへのアップロードを前提にしているわけでもありませんし、Document AIを利用するのが最適なように感じます。
Document AIのセットアップ方法
セットアップ方法の公式ドキュメントはこちらです。
一部だけご紹介すると、下記の画面から作成するプロセッサーを選択します。
今回は、一番ノーマルなDocument AIのプロセッサを作成して使います。
プロセッサの名前とリージョンを選んで作成します。
プロセッサを作成すると、下記のようにマイプロセッサの一覧から選択できるようになります。
Document AIが提供するAPIの使い方
Document AIの提供するAPIには、大きく分けてオンライン(同期的な)処理とバッチ(非同期的な)処理の2つがあります。
なんとなく想像がつくかとは思いますが、オンライン処理は1つのドキュメントを投げればリクエストがリアルタイムで返ってくるのに対し、バッチ処理は複数のドキュメントをまとめて処理してくれます。
詳しくは、こちらの公式ドキュメントも参考にしてください。
それぞれについて簡単に使い方を解説してみたいと思います。
① Document AIのオンライン処理APIの使い方
上述の通り、複数言語での使い方が公式ドキュメントに記載されているのですが、ここではcurlでの使い方を解説してみたいと思います。
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json; charset=utf-8" \
-d @request.json \
"https://{LOCATION}-documentai.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/processors/{PROCESSOR_ID}:process"
LOCATIONやPROCESSOR_IDは、プロセッサー固有の値となります。
前述の通り、2023/8現在LOCATIONはusとeuしか使えないので、作ったLOCATIONを指定します。
PROJECT_IDはGCPのプロジェクトのIDです。
上記の@request.jsonでPOSTするJSON dataは、下記のような値を指定可能です。
{
"skipHumanReview": skipHumanReview,
"rawDocument": {
"mimeType": "MIME_TYPE",
"content": "IMAGE_CONTENT"
},
"fieldMask": "FIELD_MASK"
}
skipHumanReviewは、人によるレビュープロセスを有効にするかどうかを指定します。
Human-in-the-Loop workflowを有効にすることで使えるようになるようですが、私たちは使ったことがないので、ここでは深入りしないようにします。
rawDocumentは、documentの形式であるMIME_TYPEとcontentを指定します。
contentは、バイナリ形式の画像データのをbase64でエンコーディングしたものです。
MIME_TYPEの指定方法は、公式ドキュメントをご覧ください。
上記のようにDocument AIがサポートしてる形式を一覧で確認することができます。
fieldMaskは、レスポンスに含むフィールドを指定します。
text,entities,pages.pageNumberのようにカンマ形式で指定するようです。
なにも指定しなければ、すべてのフィールドがレスポンスとして返ってきます。
レスポンスは以下のドキュメントで記載の形式で返ってきますが、ご覧いただければ分かるように少し複雑です。
{
"document": {
"uri": "",
"mimeType": "image/png",
"text": "公費負担者番号\n公費負担医療の\n受給者番号\n|患\n|者\n処\n方\n交付年月日\n備\n氏名\n考\n生年月日\n区分\n変更不可\n明大\n昭平\n処方せん\n(この処方せんは、どの保険薬局でも有効です。)\n保険者番号\nかながわ\nたろう\n神奈川 太郎\n保険医署名\n30年5月29日 男・女\n被保険者\n平成24年5月22日\n被扶養者\n処方せんの\n使用期間\n① テオドール錠200mg\n被保険者・被保険者\n手帳の記号番号\n保険医療機関の\n所在地及び名称\n電話番号\n保険医氏名\n都道府県\n番号\n調剤済年月日 平成 年 月日\n保険薬局の\n| 所在地及び名称\n保険薬剤師氏名\n14\n1日2回 朝食後、就寝前服用\n点数表\n番号\n② ツムラ麦門冬湯エキス顆粒(医療用)\n1日3回毎食間服用\n平成年月日\n③ フルタイド100 ディスカス\n1日2回吸入 ( 1 回吸入)\n以下余白\n「変更不可」欄に 「▼」 又は 「×」 を記載した\n場合は、署名又は記名押印すること。\n1\n個々の処方薬について、 後発医薬品 (ジェネリック医薬品) への変更に差し支えがあると判断した場合\nには、「変更不可」欄に 「/」 又は 「×」 を記載し、 「保険医署名」欄に署名又は記名押印すること。\n公費負担者番号\n公費負担医療の\n受給者番号\n1 2 3 4 5 6\nT235-0007\n横浜市磯子区西町 14-11\n県薬医院\n045-761-3241\n1234567\n県薬 一郎\n医療機関\n1 2 3 4 5 6 7\n特に記載のある場合を除き、\n交付の日を含めて4日以内に\n保険薬局に提出すること。\n2錠\n14日分\n9g\n14日分\n1個\n",
"pages": [
{
"pageNumber": 1,
"dimension": {
"width": 637,
"height": 812,
"unit": "pixels"
},
"layout": {
"textAnchor": {
"textSegments": [
{
"endIndex": "689"
}
]
},
"confidence": 0.96072316,
"boundingPoly": {
"vertices": [
{},
{
"x": 637
},
{
"x": 637,
"y": 812
},
{
"y": 812
}
],
"normalizedVertices": [
{},
{
"x": 1
},
{
"x": 1,
"y": 1
},
{
"y": 1
}
]
},
"orientation": "PAGE_UP"
},
"detectedLanguages": [
{
"languageCode": "ja",
"confidence": 0.72946334
},
{
"languageCode": "zh",
"confidence": 0.038531657
}
],
"blocks": [
{
"layout": {
"textAnchor": {
"textSegments": [
{
"endIndex": "22"
}
]
},
"confidence": 0.9629242,
"boundingPoly": {
"normalizedVertices": [
{
"x": 0.018838305,
"y": 0.073891625
},
{
"x": 0.15541601,
"y": 0.073891625
},
{
"x": 0.15541601,
"y": 0.13793103
},
{
"x": 0.018838305,
"y": 0.13793103
}
]
},
"orientation": "PAGE_UP"
}
},
・・・続く
今回の処方せんサンプルをリクエストした際のレスポンスは上記のようになり(省略しています)、画像内のテキストの塊ごとに分割して、その座標も返してくれていることがわかります。
OCRして画像を再構成したいようなユースケースにはよさそうです。
② Document AIのバッチ処理APIの使い方
ここでもオンライン処理同様にcurlでの使い方を解説してみたいと思います。
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json; charset=utf-8" \
-d @request.json \
"https://LOCATION-documentai.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/processors/PROCESSOR_ID:batchProcess"
LOCATIONやPROCESSOR_IDは、オンライン処理と同様にプロセッサー固有の値を参照します。
上記の@request.jsonでPOSTするJSON dataは、下記のような値を指定可能です。
{
"inputDocuments": {
"gcsPrefix": {
"gcsUriPrefix": "INPUT_BUCKET_FOLDER"
}
},
"documentOutputConfig": {
"gcsOutputConfig": {
"gcsUri": "OUTPUT_BUCKET_FOLDER",
"fieldMask": "FIELD_MASK"
}
},
"skipHumanReview": BOOLEAN
}
ご覧いただければお分かりいただけるようにinput用のGCSのバケットのフォルダーとoutput用のバケットのフォルダーを指定します。
fieldMaskやskipHumanReviewはオンライン処理と同様なのでここでは省略します。
インプットには、下記のように1つ1つのファイルのURLを指定することも可能です。
"inputDocuments": {
"gcsDocuments": {
"documents": [
{
"gcsUri": "gs://BUCKET/PATH/TO/DOCUMENT1.ext",
"mimeType": "MIME_TYPE"
},
{
"gcsUri": "gs://BUCKET/PATH/TO/DOCUMENT2.ext",
"mimeType": "MIME_TYPE"
}
]
}
}
バッチ処理のAPIでは下記のようにOPERATION_IDが返却され、このオペレーションIDを使って、バッチ処理の進捗を確認することが可能です。
{
"name": "projects/PROJECT_ID/locations/LOCATION/operations/OPERATION_ID"
}
下記のようなGETリクエストを投げて、レスポンスを見ることで、当該バッチ処理の進捗を確認することが可能です。
下記の場合、stateがSUCCEEDEDになっているので、バッチ処理は成功して終了しています。
curl -X GET \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
"https://LOCATION-documentai.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/operations/OPERATION_ID"
{
"name": "projects/PROJECT_ID/locations/LOCATION/operations/OPERATION_ID",
"metadata": {
"@type": "type.googleapis.com/google.cloud.documentai.v1.BatchProcessMetadata",
"state": "SUCCEEDED",
"stateMessage": "Processed 1 document(s) successfully",
"createTime": "TIMESTAMP",
"updateTime": "TIMESTAMP",
"individualProcessStatuses": [
{
"inputGcsSource": "INPUT_BUCKET_FOLDER/DOCUMENT1.ext",
"status": {},
"outputGcsDestination": "OUTPUT_BUCKET_FOLDER/OPERATION_ID/0",
"humanReviewStatus": {
"state": "ERROR",
"stateMessage": "Sharded document protos are not supported for human review."
}
}
]
},
"done": true,
"response": {
"@type": "type.googleapis.com/google.cloud.documentai.v1.BatchProcessResponse"
}
}
ここまでで、① オンライン処理と② バッチ処理の大まかなAPIの使い方はご理解いただけたのではないでしょうか。
RustでDocument AIのAPIを呼び出してみる
① Document AIのオンライン処理APIを呼び出してみる
では早速、RustでDocument AIのAPIを呼び出してみましょう。
ここでは、reqwestを使ってリクエストする方法をご紹介します。
let client = reqwest::Client::new();
let mut file = File::open("./prescriptions/sample.pdf")?;
let mut buf: Vec<u8> = Vec::new();
let _ = file.read_to_end(&mut buf)?;
let process_url = "https://us-documentai.googleapis.com/v1/projects/xxxxxxxxx/locations/xx/processors/xxxxxxxxx:batchProcess";
// バッファの内容をBase64形式にエンコードします
let encoded = general_purpose::STANDARD.encode(buf);
let req = RequestDocumentAi {
skip_human_review: true,
raw_document: RequestRawDocument {
mime_type: "application/pdf".to_string(),
content: encoded,
},
field_mask: None,
};
let res_body = self
.client
.post(url)
.header(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", access_token),
)
.header(
reqwest::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.json(&req)
.send()
.await?
.text()
.await?;
let res_documentai: ResponseDocumentAi = serde_json::from_str(&res_body)?;
ここではローカルのフォルダ./prescriptions/sample.pdfを開き、バイト列で読み込み、さらにそれをBase64にエンコードしています。
リクエストする際には、gcloundのaccess-tokenが必要なことにも注意をしてください。
このaccess-tokenの取得はまた少し難しいので、また別の記事で紹介したいと思います。
最後に、jsonをdeserializeすれば、上記でも紹介した、下記のような形式を取得する事が可能です。
{
"document": {
"uri": "",
"mimeType": "image/png",
"text": "公費負担者番号\n公費負担医療の\n受給者番号\n|患\n|者\n処\n方\n交付年月日\n備\n氏名\n考\n生年月日\n区分\n変更不可\n明大\n昭平\n処方せん\n(この処方せんは、どの保険薬局でも有効です。)\n保険者番号\nかながわ\nたろう\n神奈川 太郎\n保険医署名\n30年5月29日 男・女\n被保険者\n平成24年5月22日\n被扶養者\n処方せんの\n使用期間\n① テオドール錠200mg\n被保険者・被保険者\n手帳の記号番号\n保険医療機関の\n所在地及び名称\n電話番号\n保険医氏名\n都道府県\n番号\n調剤済年月日 平成 年 月日\n保険薬局の\n| 所在地及び名称\n保険薬剤師氏名\n14\n1日2回 朝食後、就寝前服用\n点数表\n番号\n② ツムラ麦門冬湯エキス顆粒(医療用)\n1日3回毎食間服用\n平成年月日\n③ フルタイド100 ディスカス\n1日2回吸入 ( 1 回吸入)\n以下余白\n「変更不可」欄に 「▼」 又は 「×」 を記載した\n場合は、署名又は記名押印すること。\n1\n個々の処方薬について、 後発医薬品 (ジェネリック医薬品) への変更に差し支えがあると判断した場合\nには、「変更不可」欄に 「/」 又は 「×」 を記載し、 「保険医署名」欄に署名又は記名押印すること。\n公費負担者番号\n公費負担医療の\n受給者番号\n1 2 3 4 5 6\nT235-0007\n横浜市磯子区西町 14-11\n県薬医院\n045-761-3241\n1234567\n県薬 一郎\n医療機関\n1 2 3 4 5 6 7\n特に記載のある場合を除き、\n交付の日を含めて4日以内に\n保険薬局に提出すること。\n2錠\n14日分\n9g\n14日分\n1個\n",
"pages": [
{
"pageNumber": 1,
"dimension": {
"width": 637,
"height": 812,
"unit": "pixels"
},
"layout": {
"textAnchor": {
"textSegments": [
{
"endIndex": "689"
}
]
},
・・・続く
② Document AIのバッチ処理APIを呼び出してみる
次にバッチ処理のAPIを呼び出す処理を紹介します。
let gcs_uri_prefix = "gs://xxxx/yyyy/zzzz/input/";
let gcs_uri = "gs://xxxx/yyyy/zzzz/output/";
let batch_url = "https://us-documentai.googleapis.com/v1/projects/xxxxxxxxx/locations/xx/processors/xxxxxxxxx:batchProcess";
let batch_req = RequestDocumentAiBatch {
input_documents: RequestInputDocuments {
gcs_prefix: RequestGcsPrefix {
gcs_uri_prefix: gcs_uri_prefix.to_string(),
},
},
document_output_config: RequestDocumentOutputConfig {
gcs_output_config: RequestGcsOutputConfig {
gcs_uri: gcs_uri.to_string(),
field_mask: None,
},
},
skip_human_review: true,
};
let res_body = self
.client
.post(url)
.header(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", access_token),
)
.header(
reqwest::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.json(&batch_req)
.send()
.await?
.text()
.await?;
let res_documentai_batch: ResponseDocumentAiBatch = serde_json::from_str(&res_body)?;
バッチ処理では、input/output用のGCSのフォルダを指定していることに注意をしてください。
最後に
今回は、RustでのDocumentAIの使い方について説明しました。
Rust × DocumentAIで解説したものの、Python以外の言語でDocumentAIを使う方の参考にもなったのではないでしょうか。
AIまわりのサービスのアップデートは激しいので、DocumentAIやGCPのドキュメントなども合わせてご確認ください。
PharmaXの採用情報について、こちらで随時更新しております。
少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話ししましょう!