はじめに
OCR(光学文字認識)とは、活字や手書き文字を読み取って文字データに変換する技術です。
請求書、レシート、名刺、免許証といった様々なものに対して、OCRのサービスが提供されています。
OCRを使用することで、データ入力の手間を少なくすることができます。
また、他のシステムと連携することで、データの有効活用も可能になります。
有名なOCRサービスに、Google Vision API(以下、Vision API)があります。
Vision APIは、Googleが提供する非常に高性能・多機能な画像分析サービスです。
※こちらに無料トライアルページがあります。
レシートの画像解析は、自動家計簿アプリなどで活用されています。
レシートを撮影すると、その商品情報や合計金額をOCRで読み取り、データを登録する仕組みです。
今回は、Vision APIを使用して簡単なレシートOCRをやってみました。
※免許証のOCRは、以下をご参照下さい。
レシートOCR
環境
環境はGoogle Colaboratoryを使用します。
Pythonのバージョンは以下です。
import platform
print("python " + platform.python_version())
# python 3.6.9
画像を表示してみよう
では、早速コードを書いていきましょう。
まずは、画像の表示に必要なライブラリをインポートします。
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib
レシートのサンプル画像も用意しておきます。
それでは画像を表示してみましょう。
img = cv2.imread(input_file) # input_fileは画像のパス
plt.figure(figsize=[10,10])
plt.axis('off')
plt.imshow(img[:,:,::-1])
Vision API セットアップ
では早速、このレシート画像をGoogle Vision APIを使用してOCRをしてみましょう。
Vision APIの料金体系について
・最初の 1,000 ユニット/月は無料(2020/06/21時点)
・料金体系の詳細はこちらを参照ください。
以下、Vision APIを使用するのに必要な準備を行います。
こちらの通りに作業を進めてください。
クライアントライブラリのインストールと、サービスアカウントキーの発行が必要になります。
クライアントライブラリのインストールは以下です。
pip install google-cloud-vision
発行したサービスアカウントキーを使用して、環境変数を設定します。
import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = json_path # json_pathは、サービスアカウントキーのパス
APIへリクエスト送信・レスポンス取得
それでは、Vision APIへリクエストを送信し、レスポンスを取得しましょう。
import io
from google.cloud import vision
client = vision.ImageAnnotatorClient()
with io.open(input_file, 'rb') as image_file:
content = image_file.read()
image = vision.Image(content=content)
response = client.document_text_detection(image=image)
エラーなく実行できれば、無事にAPIへリクエスト送信・レスポンス取得ができています。
このresponseに、Vision APIのOCR結果が入っています。
読み取った文字情報、座標の情報、確信度、言語など様々な情報が入っています。
ここでは、読み取った全文テキストの文字情報を確認してみましょう。
print(response.text_annotations[0].description)
非常に高い精度で読み取れていることが確認できます。
Vision APIは、文字の集まり具合から、画像をblockごと、paragraphごとなどに分割します。
分割されたそれぞれの領域を確認してみましょう。
まずは関数を定義します(詳細はこちらのコードを参照ください)。
from enum import Enum
class FeatureType(Enum):
PAGE = 1
BLOCK = 2
PARA = 3
WORD = 4
SYMBOL = 5
def draw_boxes(input_file, bounds):
img = cv2.imread(input_file, cv2.IMREAD_COLOR)
for bound in bounds:
p1 = (bound.vertices[0].x, bound.vertices[0].y) # top left
p2 = (bound.vertices[1].x, bound.vertices[1].y) # top right
p3 = (bound.vertices[2].x, bound.vertices[2].y) # bottom right
p4 = (bound.vertices[3].x, bound.vertices[3].y) # bottom left
cv2.line(img, p1, p2, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p2, p3, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p3, p4, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p4, p1, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
return img
def get_document_bounds(response, feature):
document = response.full_text_annotation
bounds = []
for page in document.pages:
for block in page.blocks:
for paragraph in block.paragraphs:
for word in paragraph.words:
for symbol in word.symbols:
if (feature == FeatureType.SYMBOL):
bounds.append(symbol.bounding_box)
if (feature == FeatureType.WORD):
bounds.append(word.bounding_box)
if (feature == FeatureType.PARA):
bounds.append(paragraph.bounding_box)
if (feature == FeatureType.BLOCK):
bounds.append(block.bounding_box)
return bounds
それでは、画像にそれぞれの領域を書き込んで表示してみましょう。
bounds = get_document_bounds(response, FeatureType.BLOCK)
img_block = draw_boxes(input_file, bounds)
bounds = get_document_bounds(response, FeatureType.PARA)
img_para = draw_boxes(input_file, bounds)
bounds = get_document_bounds(response, FeatureType.WORD)
img_word = draw_boxes(input_file, bounds)
bounds = get_document_bounds(response, FeatureType.SYMBOL)
img_symbol = draw_boxes(input_file, bounds)
plt.figure(figsize=[20,20])
plt.subplot(141);plt.imshow(img_block[:,:,::-1]);plt.title("img_block")
plt.subplot(142);plt.imshow(img_para[:,:,::-1]);plt.title("img_para")
plt.subplot(143);plt.imshow(img_word[:,:,::-1]);plt.title("img_word")
plt.subplot(144);plt.imshow(img_symbol[:,:,::-1]);plt.title("img_symbol")
画像をblock、paragraph、word、symbolという様々な単位で領域を分割していることが確認できました。
テキスト整形
このように、Vision APIはいい感じに領域を分割してくれるのですが、場合によってはデメリットとなることもあります。
例えば今の場合、「手巻きおにぎり辛子明太子」とその金額「*130」は分かれてしまっています。
レシートの性質上1行ごとに情報がまとまっていることが多いので、行単位で分割することを考えます。
どうすれば、行ごとに分けることができるでしょうか。
Vision APIは、1文字ごとの座標の情報(上記のsymbolのbounding_box)を持っています。
座標の値で、左から右、上から下に並び替えればうまくいきそうです。
以下、文字の座標により行ごとにまとめる処理を作成します。
def get_sorted_lines(response):
document = response.full_text_annotation
bounds = []
for page in document.pages:
for block in page.blocks:
for paragraph in block.paragraphs:
for word in paragraph.words:
for symbol in word.symbols:
x = symbol.bounding_box.vertices[0].x
y = symbol.bounding_box.vertices[0].y
text = symbol.text
bounds.append([x, y, text, symbol.bounding_box])
bounds.sort(key=lambda x: x[1])
old_y = -1
line = []
lines = []
threshold = 1
for bound in bounds:
x = bound[0]
y = bound[1]
if old_y == -1:
old_y = y
elif old_y-threshold <= y <= old_y+threshold:
old_y = y
else:
old_y = -1
line.sort(key=lambda x: x[0])
lines.append(line)
line = []
line.append(bound)
line.sort(key=lambda x: x[0])
lines.append(line)
return lines
それでは確認してみましょう。
img = cv2.imread(input_file, cv2.IMREAD_COLOR)
lines = get_sorted_lines(response)
for line in lines:
texts = [i[2] for i in line]
texts = ''.join(texts)
bounds = [i[3] for i in line]
print(texts)
for bound in bounds:
p1 = (bounds[0].vertices[0].x, bounds[0].vertices[0].y) # top left
p2 = (bounds[-1].vertices[1].x, bounds[-1].vertices[1].y) # top right
p3 = (bounds[-1].vertices[2].x, bounds[-1].vertices[2].y) # bottom right
p4 = (bounds[0].vertices[3].x, bounds[0].vertices[3].y) # bottom left
cv2.line(img, p1, p2, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p2, p3, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p3, p4, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.line(img, p4, p1, (0, 255, 0), thickness=1, lineType=cv2.LINE_AA)
plt.figure(figsize=[10,10])
plt.axis('off')
plt.imshow(img[:,:,::-1]);plt.title("img_by_line")
うまく行ごとにまとめることができました。
テキスト構造化
テキスト整形により、文字列を整理することができました。
これにより、必要な情報が取り出しやすくなりました。
必要な情報を取り出す場合には、正規表現や自然言語処理によるテキスト処理が考えられます。
今回は、正規表現を使用して、電話番号、日付、時刻、合計金額を抽出して構造化しました。
※日付、時刻の正規表現の詳細については、以下をご参照下さい。
import re
def get_matched_string(pattern, string):
prog = re.compile(pattern)
result = prog.search(string)
if result:
return result.group()
else:
return False
pattern_dict = {}
pattern_dict['date'] = r'[12]\d{3}[/\-年](0?[1-9]|1[0-2])[/\-月](0?[1-9]|[12][0-9]|3[01])日?'
pattern_dict['time'] = r'((0?|1)[0-9]|2[0-3])[:時][0-5][0-9]分?'
pattern_dict['tel'] = '0\d{1,3}-\d{1,4}-\d{4}'
pattern_dict['total_price'] = r'合計¥(0|[1-9]\d*|[1-9]\d{0,2}(,\d{3})+)$'
for line in lines:
texts = [i[2] for i in line]
texts = ''.join(texts)
for key, pattern in pattern_dict.items():
matched_string = get_matched_string(pattern, texts)
if matched_string:
print(key, matched_string)
# tel 03-1234-5678
# date 2019年10月01日
# time 08:45
# total_price 合計¥1,161
電話番号、日付、時刻、合計金額を抽出することができました。
まとめ
今回は、Google のVision APIを使用して、レシートOCRを行ってみました。
Vision APIは、非常に高精度なOCR機能を持っています。
また、個人でも気軽に使用できるOCRサービスです。
OCR結果のテキストに対して、正規表現や自然言語処理を適用することで、欲しい情報を抽出することができました。
ぜひ、サービス開発や業務効率化にご活用下さい。
なお、今回ご紹介したGoogle Vision APIとは別に、PythonにはTesseractOCRというOCRライブラリもあります。
こちらの精度(特に日本語)はVision APIに大きく劣る印象ですが、無料でありローカルで使用できます。
こちらについても以下の記事で紹介していますので、興味のある方は是非ご覧ください。
コメント
@shoku-pan
0
Vision APIの仕様変更に伴い、以下のコード部分を修正しました。
types
を使用することなく、vision.Image
と直接呼び出すことができるようになりました。