粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

スキャンしたレシートをGoogle Vision APIを使って自前でOCRしてcsv形式に変換する(Python)

はじめに

概要

レシートをScanSnapでスキャンした画像を
Google Vision APIを使ってOCRして
Pythonを使ってzaimに渡せるようなcsv形式に変換してみた

問題提起

家計管理のため、レシートをzaimという家計管理アプリでスキャンして管理しているのですが自分は定期的に撮影するということができず、200枚近くのレシートをためてしまいました。

ossyaritoori.hatenablog.com

これらの写真を撮影して...という作業に嫌気が指したのでScanSnapでまとめてスキャンしてOCRも自前でできたらいいなということで作業をはじめました。

Google Vision APIについて

Pythonで軽く試せるOCRにはいくつか選択肢があります。
前回Tesseraactを使って見た感想として、チューニングなどしないときちんと精度が出ず結構面倒だったというのがあったので
すでにある程度完成しているGoogle Vision APIというものを使ってみました。

ossyaritoori.hatenablog.com

レシートOCRに関しては下記の記事がちょうど該当したのでこちらを流用してOCRしていこうと思います。

qiita.com

実際の処理

本来は下記のようなデータフローを想定しています。

画像 -> OCR結果  -> 辞書型などのデータ -> csv形式 

しかし、Google Vision APIが従量課金制である都合上OCRをかける回数を最小にしたいため、JSONファイルを介して結果を保存して再利用します。

 (最初だけ行う) 
画像 -> OCR結果  -> JSONファイル

(試行錯誤する処理)
JSONファイル -> 辞書型などのデータ -> csv形式 

画像のOCRと保存

別記事に書いたのでそちらを参照してください。

について書いてあります。

qiita.com

OCR結果を使いやすい形式に変換

下記を流用します。

qiita.com

  • 概要:行ごとにOCR結果のテキストをまとめる
    • 入力:OCR結果のオブジェクト
    • 出力:データのリスト
  • 処理
    • テキストと位置に関する記述を抽出
    • テキストのBoundingBoxの左上の縦方向位置(Y座標)によってテキストをクラスタリング

以降ではこの関数を通して作成したlinesというリストを前提とします。

def get_sorted_lines(response,threshold = 5):
    """Boundingboxの左上の位置を参考に行ごとの文章にParseする

    Args:
        response (_type_): VisionのOCR結果のObject
        threshold (int, optional): 同じ列だと判定するしきい値

    Returns:
        line: list of [x,y,text,symbol.boundingbox]
    """
    # 1. テキスト抽出とソート
    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: #左上のBBOXの情報をx,yに集約
                        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])
    # 2. 同じ高さのものをまとめる
    old_y = -1
    line = []
    lines = []
    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

必要なテキストの抽出

最低限必要なテキストとして重要な順に下記が挙げられます。

  • 日付
  • 店名
  • 金額
  • 品目名(Optional)

これらについてそれぞれOCR結果を用いて抽出をしていきます。 思ったよりも手順が莫大になってしまったので詳細は個別記事に書きました。

日付の抽出

日付は正規表現を使うことで文字列から抽出できると考えて実装しました。

大まかにはそれで良かったのですが、OCRの都合で年や月といった文字が認識されていなかったりしたのでそういう意味では苦労しました。

qiita.com

店名の抽出

店名は基本的に最初の方にデカデカと載っているため下記の手順を考えました。

  • 上から5行分の文字列を抽出
  • その中から最も縦方向に大きなBoundingBoxを持つものを店名とする

上記は正直必ずしも正しくない方針ですが、ある程度店目に検討が付けばよいと思い強行しました。

qiita.com

金額の抽出

金額は基本的に「小計」または「合計」というキーワードと対応している...と思いましたが思ったよりも様々な表記がなされていて、フローがとても複雑になってしまいました。

qiita.com

OCR結果のcsv形式への変換

さて、最後はZaimにOCR結果をCSVでアップする段ですが、下記のように項目ごとに列を指定できるので普通のcsv形式に変換しちゃって良さそうです。

ファイルアップロード時の画面

Pandasでちょちょっと書けば終わりなのと後述の理由もあってやる気をなくしたので省略させてください。

オチ

散々いろいろやって気づいたのですがZaimは有料会員にならないとカテゴリをcsvから読んでくれません。
ということでこれではカテゴリの管理ができません…。アホかと…。

一応、それっぽくまとめたものを下記においておきます。

github.com