はじめに
GoogleのVision APIを使用してレシートのOCRをしてみました。
言語はPython、環境はGoogle Colaboratoryを使用しています。
実装
入力画像
OCRする画像を表示してみます。
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の設定
Vision APIを使用するには、あらかじめアカウントキーを作成する必要があります(詳細)。
# 以下実行後、リスタートが必要
!pip install --upgrade google-cloud-vision
# 環境変数
%env GOOGLE_APPLICATION_CREDENTIALS = json_path # json_pathはアカウントキーのパス
動作確認してみます。
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")
テキスト整形
このように、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