🎉

レシート読取

2022/05/21に公開

OCRソフトが壊れた

いつも使っているOCRソフトがとうとうiOS15.4.1に対応しなくなった。
とても困った。レシート読取結果を家計簿に登録していたからだ。
副回線の楽天モバイルはAndroidだ。Google Playにアプリはなかった。詰んだ。
旅行中にたまったレシートが処理しきれない。

新しいスキャナを買う時期かもしれない。
連続読込のある高性能なスキャナは2~3万円以上する。

高い。

買えない額ではないが、すでに家にプリンター兼スキャナがある。
意気込んで買ったプリンターは大量にインクが余り、買い替えるのは忍びない。

OCRソフトに不満がなかったわけではない。
複数枚読取に難があり、結局一枚ずつ撮影して手間がかかっていた。

家にはA4サイズのスキャナーがある。
在宅勤務で暇も持て余している。婚活は、マッチングアプリの無料期間がもうすぐ終了する。

勉強の意味で、スキャナー取り込み~家計簿ソフトへの登録を半自動化してみよう。

技術記事を書く作業は、すでに2回ウェブページから作ろうとして挫折した。
自前で全部やりたがる癖があるのは技術系ならよくある話なのだろう。
身の程を知り、外部リソースで執筆してみるのもよいと思えてきた。

前置きがとても長い。

本投稿は、解析系を作りながら執筆したくて書き始めることにした。
なるべく最初に計画だててから、適宜戻って修正していくスタイルでやろうと思う。

さっそく問題点に気づいた。画像データに細かい生活情報が記載されている。
...マスク画像を出力して適宜ボカせるところをボカしていく。まぁ大体の地域だけだし、そんなに読まれないからとりあえずすごく人気が出たら考えよw

開発環境

PC: Windows 11 / WSL2
スキャナー: EPSON PX-M160T

計画

-スキャナー取込み
-矩形抽出
-OCR
-データ構造化・家計簿ソフトのフォーマット化

どこまでできるか。挫折せずに続けられるといいなぁ。

スキャナー取込み

撮影時に、複数パターンを用意した。
スキャンしてみて、背景が白いとレシートの輪郭がうまく抽出できないことがわかった。
背景とレシートが同化してしまった。
白背景

そこで、手じかにあったゴミ袋を使ってスキャンしてみた。
ゴミ袋

反射して、使いにくそうだ。

最終的に、ユニクロのヒートテックが良い感じに反射せず使えそうだった。もう少し薄い布を使ってもいいかもしれない。

衣類

矩形抽出

矩形抽出にあたり、難所があった。
-白背景でレシートを撮影するとレシートの輪郭線がとりにくい←最初に解決
-傾いた矩形抽出でのAffine変換: ndArray, OpenCVの数理処理, matplotlibの描画において座標軸の取り方が違う

ここの部分に頭の整理が追いつかず、かなり時間がかかってしまった。
最初からまっすぐ撮影するように運用したらよいのに、と後悔した。

Python OpenCV(4.5.5)を使うことにした。以下にコードを清書する。
神々がいたのでそれらの関数を参照する。
https://jdhao.github.io/2019/02/23/crop_rotated_rectangle_opencv/
https://github.com/leimao/Rotated-Rectangle-Crop-OpenCV/

import cv2
import os
import glob
import numpy as np
import re
import matplotlib.pyplot as plt
import scipy.ndimage as ndimage

# $ wget https://github.com/leimao/Rotated-Rectangle-Crop-OpenCV/blob/master/rotated_rect_crop.py
from rotated_rect_crop import *

########################
f = "/mnt/c/Users/username/Documents/receipt005.jpg"
save_dir = '/mnt/c/Users/username/Documents/receipt005_cropped.jpg'

########################
def crop(img, rect):
    img_rows = img.shape[0]
    img_cols = img.shape[1]
    box = cv2.boxPoints(rect).astype(np.int0)
    rect_bbx_upright = rect_bbx(rect = rect)
    rect_bbx_upright_image = crop_rectangle(image = img, rect = rect_bbx_upright)
    rotated_angle = rect[2]
    rotated_rect_bbx_upright_image = image_rotate_without_crop(mat = rect_bbx_upright_image, angle = rotated_angle)
    rect_width = rect[1][0]
    rect_height = rect[1][1]
    crop_center = (rotated_rect_bbx_upright_image.shape[1]//2, rotated_rect_bbx_upright_image.shape[0]//2)
    final = rotated_rect_bbx_upright_image[int(crop_center[1]-rect_height//2) : int(crop_center[1]+(rect_height-rect_height//2)), int(crop_center[0]-rect_width//2) : int(crop_center[0]+(rect_width-rect_width//2)),:]
    return final

def get_contours(f, limit_area=100):
    img = cv2.imread(f, cv2.IMREAD_GRAYSCALE)
    ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    th_fill = (ndimage.binary_fill_holes(th2).astype(int) * 255).astype(np.uint8)
    contours, _ = cv2.findContours(th_fill, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    contours = list(filter(lambda x: cv2.contourArea(x) > limit_area, contours))
    return contours

########################
contours = get_contours(f)
cnt = max(contours, key = cv2.contourArea)
rect = cv2.minAreaRect(cnt)

res = crop(img, rect)
res = cv2.rotate(res, cv2.ROTATE_90_CLOCKWISE)
cv2.imwrite("cropped/final.jpg", res)

途中、動作確認のため、中間的に下記のようなファイルを出力した。
これは、別途記事にまとめたらよい気がするが、いったんここで記載する。
下記で、白黒画像の行列を扱っていたのが原因で、線が描画できず、ずっと作業が止まっていた。
白黒画像をBGR画像として読み込みなおしたら直った。

輪郭線描画

輪郭線を表示して保存

img = cv2.imread(f)
# NG!! img = cv2.imread(f, cv2.IMREAD_GRAYSCALE)

box = cv2.boxPoints(rect)
box = np.int0(box)

cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
cv2.imwrite("cropped/receipt005_boxed.jpg", im_boxed)

最初に参照していた記事だと、なぜか端のほうが見切れてしまい、頓挫していた。
その後、文献調査を深めることで、良い感じに関数にまとめてくれているGitHubリポジトリを発見した。GitHubには神がいることを再認識。スターをつけておいた。

良い感じに矩形抽出できるようになった。

ここから、OCR関数に投げ込んだ。
結論として、EasyOCRが一番成績が良かった。何より、行ごとに分割しなくてよいところがとてもよかった。

import easyocr
reader = easyocr.Reader(['ja','en'])
receipt = reader.readtext('cropped/final.jpg')

TrOCRはFine Tuningがよく分かっておらず、またレシートデータの数も足りていないのでいったん保留することにした。
EasyOCRでデータをためていき、適宜修正を加えてからTrOCRのFine Tuningに移行するのがよさそうな気がしてきた。

とりあえず、今日(2022月5月4日)に画像撮影からOCRまで終わったので、いったん記事にまとめやすいタイミングになった。

本当は、この後にZaimの形式に直す部分もあるのだが、それは追々実施する。マネーフォーワードは2018/12/06でCSVアップロードサービスは終了したらしい。https://support.me.moneyforward.com/hc/ja/articles/900003501806-CSV-Excelによるデータのアップロードはできますか

そう考えると、zaimはNTT系列の会社ということもあり、一定水準の品質のアウトプットが出てきていることを実感する。

結局、撮影条件等の準備があるから、それぞれの条件でチューニングする必要があるため、いったんは自分で作っていく必要がある程度あるのかもしれない。

そのうち、フリーで使えるOCRが出てくればいいなーと思いつつ、作業はここまでにする。
色々作業を進めると、結局時間給単位で考えたらスキャナを買ってしまえばいい感じになりそうだが、勉強代ということで我慢。買わなくてよいものを買わなかったということでお金が節約されたと思っておこう。

あと、Googleレンズすごい。

Discussion