LoginSignup
148

posted at

updated at

Organization

【Colab】Vision APIでレシートOCR

はじめに

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])

image.png

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)
SAVERSONICS
セブン-イレブン
千代田店
東京都千代田区二番町8-8
電話:03-1234-5678
レジ#31
2019年10月01日(火) 08:45責012
領取書
手巻おにぎり辛子明太子
コカコーラ500ml
パラドゥミニネイル PK03
メビウスワン
50円切手
*130
*140
300
490、
50年
小計(税抜8%)
¥270
消費税等(8%)
¥21
小計(税抜10%)
¥300
消費税等(10%)
¥30
小計(税込10%)
¥490
小計(非課税)
¥50
合計 ¥1,161
(税率 8%対象
¥291)
(税率10%対象
¥820)
(内消費税等8%
¥21)
(内消費税等10%
¥74)
キャッシュレス還元額
-22
nanaco支払
¥1,139
お買上明細は上記のとおりです。
nanaco番号
*******9999
今回ポイント
2P
[*]マークは軽減税率対象です。

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")

image.png

テキスト整形

このように、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")
セブン-イレブン
SAVERSONICS
千代田店
東京都千代田区二番町8-8
電話:03-1234-5678レジ#31
2019年10月01日(火)08:45責012
領取書
手巻おにぎり辛子明太子*130
コカコーラ500ml*140
パラドゥミニネイルPK03300
メビウスワン490、
50円切手50年
小計(税抜8%)¥270
消費税等(8%)
¥21
小計(税抜10%)¥300
消費税等(10%)¥30
小計(税込10%)¥490
小計(非課税)¥50
合計¥1,161
(税率8%対象
¥291)
(税率10%対象¥820)
(内消費税等8%¥21)
(内消費税等10%¥74)
キャッシュレス還元額-22
nanaco支払¥1,139
お買上明細は上記のとおりです。
nanaco番号*******9999
今回ポイント2P
[*]マークは軽減税率対象です。

うまく行ごとにまとめることができました。

必要な情報を抜き出す

正規表現を使用して、電話番号、日付、時刻、合計金額を抽出してみます。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
148