はじめに
アクセンチュアの赤石です。必要があって、OCR機能をいろいろ調べています。
Google では、Google Vision APIとGoogle Driveの機能があります。
Google Vision APIの記事
Google Driveの記事
大量にOCRをしたい場合は、普通に考えるとAPIとして使えるGoogle Vision API一択なわけですが、どうも軽くテストした限り、Google Drive APIの方が認識精度が高いみたいなのです。そもそも、同じグーグルで同じ機能のエンジンが2つあることからして謎なのですが。。。
それで、普通であればUI経由で使うGoogle DriveのOCR機能をAPIで使いたいと思ってしまったわけです。
結論として、頑張ればGoogle DriveのOCR機能をAPIで使うことは可能でした。
当記事は、そのための手順を示すものとなります。
呼び出し方法と処理の流れ
Google Driveには、Google Drive APIというAPIがあります。基本的には、これを使います。
大きな流れとしては、ローカルのイメージファイルをGoogle Drive上にアップロードします。この時に、
ソース:イメージファイル
宛先:Google DOC
のような形にします。更にこの時に、うまくオプションを指定すると、OCRも同時に実行可能です。
ここでできた、OCR読み取り後のテキストを含むクラウド上のGoogle DOC文書を、テキスト形式でローカルにExportします。
すると、めでたくOCRの読み取り結果だけが取得できるのです。
資材関係
以下で紹介するコードの全量は次のgithubにあります。
Notebookリンク
また、サンプル手書きイメージは、
にあります。
事前準備
なんといっても、Google APIを使う場合の最大のハードルは、認証関係のところではないのでしょうか?
ここのステップを細かく書くと、それだけで記事の量が膨大になってしまうので、他の記事を紹介して逃げます。
大きな流れとして、Gogleのアカウントを作り、プロジェクトを作り、利用したいサービスを有効にし、最後に credentailの入ったjsonファイルを作ります。下記の記事などを参考にして下さい。
Pythonでgoogle-drive-ocrを使用し文字認識を行う
当記事では、こうやってできた認証情報を含んだjsonファイルをgoogle-drive-api.json
にリネームして使うことを前提にしています。
ちなみに、上記の記事の目的も当記事と同じOCR処理で、ここではGoogle Drive APIのラッパー関数を使った手順が紹介されています。つまり、お手軽に動かしてみることが目的なら、上の記事を参照した方が早いです。ここから説明するのは、素のGoogle Drive API呼び出しだけで、OCR処理をする手順ということになります。
コード解説
では、早速コードの解説に入りましょう。
初期設定
まずは、必要なライブラリの導入です。全部notebookの中で実行するために、セルの中で!pip xxx
の形で導入しています。
# ライブラリ導入
!pip install google-api-python-client | tail -1
!pip install google-auth-httplib2 | tail -1
!pip install google-auth-oauthlib | tail -1
次にライブラリのインポートです。こちらに関しては、普通のライブラリとGoogle API関連でセルを分けてみました。
# ライブラリインポート Python 一般
import io
import os.path
import matplotlib.pyplot as plt
from PIL import Image
# ライブラリインポート Google API関係
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from googleapiclient.http import MediaIoBaseDownload
次にGoogle API呼び出しで必要な変数をまとめて定義します。おまじないみたいなものだと思って下さい。
# 変数定義
SCOPES = ['https://www.googleapis.com/auth/drive.file']
MIME_TYPE = 'application/vnd.google-apps.document'
APPLICATION_NAME = 'ipa-google-drive-api-client'
認証取得とサービス生成
上の操作で取得した認証情報の入ったjsonを使ってcred変数を取得します。次にcred変数を引数にサービスの生成をします。私もこれ以上詳しいことはわかりません。以下のコードもちょっと長めのおまじないと思って下さい。
これは以下のコードすべてに対して言えることですが、クイックに結果を出すことを目的にしているので、エラー処理関係は一切無視していることをあらかじめお断りしておきます。
# Google Drive API用サービス取得
def get_service():
# credentialの取得
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'google-drive-api.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token.json', 'w') as token:
token.write(creds.to_json())
# serviceの取得
service = build('drive', 'v3', credentials=creds)
return service
上のセルは、サービス取得までを関数の形で定義しました。この関数を呼び出すと、serviceのインスタンスがかえってきます。正常に終わった場合は、カレントディレクトリにtoken.json
ファイルができているはずです。
read_ocr関数
サービスの生成までできれば、いよいよ APIを使ったGoogle Driveの操作ができます。ここで紹介するサンプルコードでは、OCRイメージからテキストを得るまでの処理全体を read_ocr関数として定義しました。
この処理は冒頭で説明したように、「イメージのGoogle Driveへのアップロード」と「クラウド上のGoogle DOCのローカルへのエクスポート」にわかれます。それぞれ個別に説明します。
アップロード処理
関数内のアップロード処理部分を抜き出すと下記のようになります。
# ファイルのアップロード
# ローカルファイルの定義
media_body = MediaFileUpload(input_file, mimetype=MIME_TYPE, resumable=True)
# Google Drive上のファイル名
newfile = 'output.pdf'
body = {
'name': newfile,
'mimeType': MIME_TYPE
}
# creat関数でファイルアップロード実行
# 同時にOCR読み取りも行う
output = service.files().create(
body=body,
media_body=media_body,
# ここで読み込み先言語の指定を行う
ocrLanguage=lang,
).execute()
まず、ローカルファイルを表すmedia_body
インスタンスを、MediaFileUpload
クラスを用いて生成します。
このインスタンスを引数にservice.files().create
関数を呼び出します。この際にocrLanguageオプションを指定するのがミソでこれによって、アップロードしてできたDOCファイルには、元々のイメージ以外に、OCR読み取り結果のテキスト情報も含まれるようになります。
エクスポート処理
アップロードが終わったら、今作った文書に対してエクスポート処理をします。具体的なコードは次のとおりです。
# テキストファイルのダウンロード
# リクエストオブジェクト生成
request = service.files().export_media(
fileId=output['id'],
mimeType = "text/plain"
)
# 出力用テキストファイル名
output_path = 'output.txt'
fh = io.FileIO(output_path, "wb")
downloader = MediaIoBaseDownload(fh, request)
done = False
while done is False:
status, done = downloader.next_chunk()
# Google Drive上のファイル削除
service.files().delete(fileId=output['id']).execute()
# テキストの取得
with open(output_path) as f:
mylist = f.read().splitlines()[1:]
今度はservice.files().export_media
関数で、 request変数を定義するのですが、ここで、mimeType = "text/plain"
を指定するのがミソです。こうすることで、もともとアップロード時に含まれていたイメージデータはなくなってしまい、OCR読み取り結果の文字情報だけが抽出されることになります。
この後で、MediaIoBaseDownload(fh, request)
クラスのインスタンスを生成すると、ファイルハンドルと、リクエストが結合され、エクスポートデータの書き込み先が、このファイルになります。
あとは、通常の方法で、テキストファイルを読み込みます。最初の1行は"---"のような文字が入っているので、後処理でその分を取り除きます。
これで、元々やりたかった、イメージデータに対するOCR文字認識の結果を抽出することができました。
利用例
それでは、最後に利用サンプルを示します。英語ケース、日本語ケースの両方を試してみました。
サービス生成
最初に関数を使って、API呼び出しに必要なサービスインスタンスを生成します。
# サービスインスタンスの生成
service = get_service()
英語テスト結果
まず、英語の認識をしてみましょう。実装コードと結果を以下に示します。
Notebookで実装を細かく見ていただけるとわかりますが、今回定義したread_ocr関数
はデフォルト言語を'en'
にしているので、3つめの引数なしで呼び出すと、自動的に英語認識になります。
コードの後半では、 OCRの結果得られたテキストと、元イメージを表示して、見比べることができるようにしています。
# 英語の場合
# イメージからOCR読み込み
input_file = 'ocr-sample.jpg'
output = read_ocr(service, input_file)
# 結果確認
# テキストの表示
print(output)
# 元イメージの表示
img = Image.open(input_file)
plt.figure(figsize=(10,10))
plt.imshow(img, cmap='gray')
plt.axis('off')
plt.show()
著者の下手くそな字で申し訳ありません。OCRの能力を試すため、わざとヘタめに書いてみました(ウソ)。
3つめの文の最後がピリオドでなくカンマになってしまっていますが、これはどちらかというと書き手の問題ですか。
文字の認識という意味では100%でした。素晴らしい!
日本語テスト結果
次に日本語の書かれたイメージデータに対して、同じ処理をしています。今度は、read_ocr関数
の3つめの引数に'ja'
を指定しています。
# 日本語の場合
input_file = 'ocr-sample-jp.jpg'
output = read_ocr(service, input_file, 'ja')
# 結果確認
# テキストの表示
print(output)
# 元イメージの表示
img = Image.open(input_file)
plt.figure(figsize=(10,10))
plt.imshow(img, cmap='gray')
plt.axis('off')
plt.show()
今回も完璧でした!
さすがGoogleさん、OCRという一見地味なタスクでも、いい仕事をしているみたいです。
結果まとめ
OCRというのは、すごく歴史のある技術で、しかし、今まではどうしても一定比率で誤認識が出てしまい、その後処理をどうするかで業務利用時は結構ややこしかったというのが私の理解です。
しかし、ここまで認識精度が上がってくると、そのあたりのところを軽減できて真の意味での工数削減ができるかもと思いました。絶対100%にはならないと思うので、OCRエンジン側で自信のないケースは、確信度のような数値が取れるといいのでしょうか?
あと、今回の方式、精度はいいのですが、やはりローカルのファイルのアップロード、ダウンロードと1往復している影響か、処理時間という観点では、今一つでした。全体の処理時間を短くするためには、全部クラウドで完結して処理するとか、後半のテキスト抽出は、別バッチでまとめてやるとか、もう一工夫必要な感じがしました。