Ateam cyma Advent Calendar 2019、21日目です!
株式会社エイチーム EC事業本部サイマのエンジニア @shimura_atsushi が2回目の登場です。
1回目でGoogle Could PlatformのOCRサービスを使ってサイマの課題に挑戦するとしてサイマの抱える課題に立ち向かう1手目の挑戦をしました。
2回目である今回は更に納品書チェックに対して踏み込んだ取り組みをしていきます。
前回をおさらい
前回の投稿でGCPのサービスを利用して単純なOCRを試しました。しかし、今サイマで運用されている納品書類は文面が複雑なものが多くただOCRにかけただけでは文字おこしの精度は低く例えうまく文字おこしされたとしてもデータのラベリングがされておらず文字データの再利用性が乏しい状態でした。
今回は・・・
前回の反省を基に今回は「OCRをしやすい画像が用意する」にフォーカスしてOCRにかける画像の前処理を作成していきます。
内容は前回からの継続ですのでタイトルはそのままですが今回はPython
での実装がメインでGoogle Could Platform
に関しては薄めですあしからずご容赦ください。
今回はサイマのエンジニアである@NamedPythonさんの勧めもあって画像処理ライブラリが充実しているPython
を利用して行きます。
環境を準備する
今回用いた開発端末
- 開発端末 MacBook Pro 15-inch
- OS macOS Mojave
もろもろインストールする
ここらへんはサクッと行きます。
インストール
-
Python
をインストールする-
pyenv
- pythonのインストールするバージョンを管理できる
-
python 3.8.0
- 執筆時点の最新を利用
-
pip
- pythonにおけるパッケージ管理ツール
- pythonをインストールすると一緒に入ると思います
-
-
pdf2image
インストール- PDFからPNGやJPEGへ変換するのに利用
-
poppler
インストール- pdf2imageでPDF変換に利用
-
pillow
インストール- 画像の加工に利用、主に切り抜きに利用
-
opencv
インストール- 画像の加工に利用、主に2値化に利用
brew install pyenv
pyenv install --list #インストール可能なバージョンを確認
pyenv install 3.8.0
pip3 install pdf2image
brew install poppler
pip install pillow
pip install opencv
大まかな流れ
- 書類を複合機にてスキャン
- スキャンデータ(PDF)を画像データに変換する
- 変換した画像データを切り抜く
- 切り抜いたデータの2値化する
- OCRにかける
複合機にてスキャン(PDF)
本社にある複合機を利用します、スキャンすると登録されているメールアドレスにPDFが添付されてきます。
※最終的な運用ではサイマの各工場でスキャンをする予定
PDFから画像データへ変換する
スキャンされたデータはPDF形式なので画像データに変換します。
ディレクトリを指定すると格納されたPDFファイルを画像データに変換してくれるというものです。
pdf2image
をインポートしてconvert_from_path
というメソッドに変換したいファイルパスを渡せば
変換してくれます、簡単ですね。
from pdf2image import convert_from_path
from pathlib import Path
import os
p = Path('./img/pdf')
pdf_list = os.listdir(p)
print(pdf_list)
for i, pdf_file_path in enumerate(pdf_list):
images = convert_from_path('./img/pdf/{}'.format(pdf_file_path))
for image in images:
image.save('./img/png/{}.png'.format(i), 'png')
変換したデータを切り抜く
今回のOCRの肝はこの工程です。
前回の反省を基に複雑な納品書のデータから必要な箇所を切り取りラベリングするという処理を本工程で実装します。
設定ファイルをJSONで用意する
納品書のフォーマットは仕入先ごとに基本的には一定(自転車とパーツで別パターンはある)なので切り抜くために必要な座標をJSON形式の設定ファイルを納品書フォーマットごとに用意します。
納品書上の内容で必要な情報は
- 仕入先名
- 納品日
- 商品番号
- 個数
- 単価
ですのでこれらが記載される箇所の座標を設定ファイルに持っておきます。
各仕入先ごとのJSON設定ファイル
{
"wholesaler_id": 2,
"warehouse": {
"x":10,
"y":10,
"height":50,
"width":100
},
"date": {
"x":20,
"y":20,
"height":50,
"width":100
},
"product": {
"x":30,
"y":30,
"height":150,
"width":200
},
"figure": {
"x":40,
"y":40,
"height":200,
"width":250
},
"price": {
"x":50,
"y":50,
"height":200,
"width":250
}
}
※数字は仮のものを記載してます。
pillowを用いた画像切り抜き処理
from PIL import Image
import sys
import json
import productsetting
args = sys.argv
p = productsetting.product.ProductSetting(args[1])
image = Image.open('img/png/{wholesaler_id}.png'.format(wholesaler_id=p.wholesaler_id))
rect = (
p.warehouse['x'],
p.warehouse['y'],
p.warehouse['x'] + p.warehouse['width'],
p.warehouse['y'] + p.warehouse['height']
)
print(rect)
cropped_image = image.crop(rect)
cropped_image.save('{wholesaler_id}.png'.format(wholesaler_id=p.wholesaler_id))
JSONによる設定ファイルを読み込むクラス
import sys
import json
class ProductSetting:
CONFIG_SETTING_FILE_BASE_FORMAT = './settings/product/{wholesaler_id}.json'
def __init__(self, wholesaler):
config_file_path = open(self.CONFIG_SETTING_FILE_BASE_FORMAT.format(wholesaler_id=wholesaler), 'r')
config = json.load(config_file_path)
self.wholesaler_id = config['wholesaler_id']
self.warehouse = {
'x': config['warehouse']['x'],
'y': config['warehouse']['y'],
'height': config['warehouse']['height'],
'width': config['warehouse']['width']
}
self.product = {
'x': config['product']['x'],
'y': config['product']['y'],
'height': config['product']['height'],
'width': config['product']['width']
}
self.date = {
'x': config['date']['x'],
'y': config['date']['y'],
'height': config['date']['height'],
'width': config['date']['width']
}
self.figure = {
'x': config['figure']['x'],
'y': config['figure']['y'],
'height': config['figure']['height'],
'width': config['figure']['width']
}
このスクリプトを実行するとこのような画像から
※画像は加工してあります。
切り抜いたデータを2値化する
次は切り取った画像のOCR精度を高めるために文字を2値化して読み取り精度の向上を図ります。
opencv
を利用して作成
2値化のプログラムはシンプルにこんな感じで
import cv2
img = cv2.imread('./result/png/1013/buyoption_1013.png', 0)
threshold = 100 #しきい値
ret, img_thresh = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)
cv2.imwrite('./result/deeply/test/buyoption_1013.png', img_thresh)
サンプルが良くないのかあまり恩恵を感じられないです。
なんと!よりはっきりとした画像になりました。
この画像を前回作成したGCPのOCRにかけてみます。
その結果・・・
このように文字起こしされました、よく考えると「配達日」という箇所もノイズになるので省いても良かったですね。
ただこの精度ならチェックに関する再利用性が保てそうです。
まとめ
今回はOCRに掛ける前処理として、
- 必要な箇所だけの切り抜き
- 切り抜いた画像の2値化による鮮明化
をすることで如何にOCRに対して有利な状況を作れるかに挑戦してみました。
今回の事務処理効率化の取り組みですが事業部内で「やってみます!」と豪語したは良かったですが、納品書の現物を見ると本当に自動化できるのかと不安でした。
結果、画像の切り抜きによるノイズの除去と2値化による鮮明化の後でOCRにかけることで、精度が高めることでき、自動化が現実的になったかなと実感してます。
アドベントカレンダー2回にわたりサイマにおける納品書チェックという課題にOCRを中心とした技術で自動化へ挑戦をはじめました。今後はシステムの実装を進めつつ工場を巻き込んた運用の実現に取り組みたいです。
最後に
Ateam cyma Advent Calendar 2019の21日目いかがでしたか。
22日目はサイマのデザイナである@ryo_cyさんがBEMを用いたCSS設計について話をしてくれますのでお楽しみに!
株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。
エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。