続 Google Cloud PlatformのOCRサービスを使ってサイマの課題に挑戦する

Ateam cyma Advent Calendar 2019、21日目です！
株式会社エイチーム EC事業本部サイマのエンジニア @shimura_atsushi が２回目の登場です。

１回目で[Google Could PlatformのOCRサービスを使ってサイマの課題に挑戦する](https://qiita.com/shimura_atsushi/items/cb29b1d3841fc21d460b)としてサイマの抱える課題に立ち向かう１手目の挑戦をしました。
２回目である今回は更に納品書チェックに対して踏み込んだ取り組みをしていきます。

# 前回をおさらい
[前回の投稿](https://qiita.com/shimura_atsushi/items/cb29b1d3841fc21d460b)で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
```

## 大まかな流れ
1. 書類を複合機にてスキャン
2. スキャンデータ（PDF）を画像データに変換する
3. 変換した画像データを切り抜く
4. 切り抜いたデータの2値化する
5. OCRにかける

## 複合機にてスキャン（PDF）
本社にある複合機を利用します、スキャンすると登録されているメールアドレスにPDFが添付されてきます。
※最終的な運用ではサイマの各工場でスキャンをする予定

## PDFから画像データへ変換する
スキャンされたデータはPDF形式なので画像データに変換します。
ディレクトリを指定すると格納されたPDFファイルを画像データに変換してくれるというものです。
`pdf2image`をインポートして`convert_from_path`というメソッドに変換したいファイルパスを渡せば
変換してくれます、簡単ですね。

```pdf2png.py
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設定ファイル
``` shiiresaki_setting.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を用いた画像切り抜き処理
```crop4image.py
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による設定ファイルを読み込むクラス

```productsetting.py
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']
    }    
```

このスクリプトを実行するとこのような画像から
![スクリーンショット 2019-12-19 22.11.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/e487065a-66c3-30b3-ff21-0b4601603abb.png)
※画像は加工してあります。

このように指定した座標で切り抜くことができました。
![buyoption_1013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/14fc5db9-bd4e-4ac9-fc00-5eaa4724f5ec.png)

## 切り抜いたデータを2値化する
次は切り取った画像のOCR精度を高めるために文字を2値化して読み取り精度の向上を図ります。

`opencv`を利用して作成
2値化のプログラムはシンプルにこんな感じで

```deeply_character.py
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)
```
切り取ったこんな画像が
![buyoption_1013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/868786e9-7d85-974c-2d02-0c003cd5f5fa.png)

こんな感じに2値化されました
![buyoption_1013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/252addc2-58a8-20f0-98ce-069d70896646.png)

サンプルが良くないのかあまり恩恵を感じられないです。

手強そうなこの画像で試してみようと思います。
![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/5b700f10-b220-3a10-6ad7-9c94fe05e2f8.png)

しきい値を調整して2値化すると・・・
![buyoption_1013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/0dc3eaca-e8e7-bc53-d3ed-26b276972413.png)

なんと！よりはっきりとした画像になりました。

この画像を[前回作成](https://qiita.com/shimura_atsushi/items/cb29b1d3841fc21d460b)したGCPのOCRにかけてみます。
その結果・・・
![スクリーンショット 2019-12-19 20.24.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311437/f7f8a647-8bf6-0eb4-6153-a98c6ee4f61a.png)

このように文字起こしされました、よく考えると「配達日」という箇所もノイズになるので省いても良かったですね。
ただこの精度ならチェックに関する再利用性が保てそうです。

# まとめ
今回はOCRに掛ける前処理として、

- 必要な箇所だけの切り抜き
- 切り抜いた画像の2値化による鮮明化

をすることで如何にOCRに対して有利な状況を作れるかに挑戦してみました。

今回の事務処理効率化の取り組みですが事業部内で「やってみます！」と豪語したは良かったですが、納品書の現物を見ると本当に自動化できるのかと不安でした。
結果、画像の切り抜きによるノイズの除去と2値化による鮮明化の後でOCRにかけることで、精度が高めることでき、自動化が現実的になったかなと実感してます。

アドベントカレンダー2回にわたりサイマにおける納品書チェックという課題にOCRを中心とした技術で自動化へ挑戦をはじめました。今後はシステムの実装を進めつつ工場を巻き込んた運用の実現に取り組みたいです。

# 最後に
Ateam cyma Advent Calendar 2019の21日目いかがでしたか。
22日目はサイマのデザイナである@ryo_cyさんがBEMを用いたCSS設計について話をしてくれますのでお楽しみに！

[株式会社エイチーム](https://www.a-tm.co.jp)では、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方は[cymaのQiita Jobs](http://bit.ly/35E1I08)をご覧ください。

そのほかの職種は、[エイチームグループ採用サイト](http://bit.ly/2XQpd3q)をご覧ください。