AWS Lambda上でOCRして文字の位置を特定したい人生だった。

# やりたいこと

- OCRを使って文字の位置を検出したい
- そんなに頻繁に使うものじゃないからLambdaで動かしたい
- Webから使いたい

というわけで、できましたー


# 文字位置特定 with OCR on AWS Lambda

リポジトリは[こちら](https://github.com/shinichi-takahashi/lambda-ocr-detect-positions)

## tesseractってなに？

- OCRを行ってくれるソフトウェア
- Macにはbrewで入る(v3.04)
- 文字を取得するだけじゃなくて文字の位置をhOCR(html)やtsv形式で出力できる <- 重要

## どうやってLambdaで動かすの？

- [StackOverflow](http://stackoverflow.com/questions/33588262/tesseract-ocr-on-aws-lambda-via-virtualenv/35724894#35724894)を参考に...
- Lambdaではスタンドアローンなバイナリファイルや.soをちゃんと耳そろえてアップすれば動く
- subprocess(Pythonのコマンドライン実行ライブラリ)も動く

つまり...!!
*tesseractと一緒にアップすればLambdaでOCRが動く!!*

ちなみに、AmazonLinux上でビルドしないとPillow(PIL)がELF headerがないとかいう妖精さんの首がもげる現象に立ち会い無事死ねます。


## 文字列の位置どうやって検出するの？

- 一般的なOCRでは文字をテキストで返してくれることがほとんど
- [Doc](https://github.com/tesseract-ocr/tesseract/wiki/Command-Line-Usage)を読むとv3.05ではtsv形式がサポートされてるくさい
- 普通にインスコするとtesseract(v3.04)が入っちゃう
- v3.05を使うべくStackOverflowのとおりに手でビルドしないといけない

これはつらかった。

というわけでいい加減、どうやって導入したかかきます。


#　インストール

全部ec2-userでいいです。

## 必要なパッケージのインストール

```bash
sudo yum install -y gcc gcc-c++ make
sudo yum install -y autoconf aclocal automake
sudo yum install -y libtool
sudo yum install -y libjpeg-devel libpng-devel libtiff-devel zlib-devel
sudo yum install -y git
```

## nvmのインストール

AmazonLinuxではyumでいれたnodeのバージョンが古すぎて色々(後述)つらいのでnvmいれておきます。
とはいえAmazonLinuxじゃないとビルドしたところでLambdaでエラーはいてしまうので頑張りましょう。

```bash
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
$ source ~/.bashrc 

$ nvm install v6.9.4  
$ nvm alias default v6.9.4  

# バージョンを確認
$ npm -v
$ node -v
```

## Leptonicaのインストール

Leptonica is 画像解析とかやってくれるOSSでtesseract動かすのに必要
ここのバージョンをあげないとtesseractのv3.05も使えない

```bash
$ cd ~
$ mkdir leptonica
$ cd leptonica

$ wget http://www.leptonica.com/source/leptonica-1.74.tar.gz

# unzip
$ tar -zxvf leptonica-1.73.tar.gz
$ cd leptonica-1.73

# build
$ ./configure
$ make
$ sudo make install
```

## Tesseractのインストール

まだv3.05はdevなのでリリースに乗ってない == zipが落ちてないのでcloneして頑張ります。

```bash
$ cd ~
$ git clone https://github.com/tesseract-ocr/tesseract.git
$ cd tesseract/
$ git checkout -b 3.05 origin/3.05

# initialize
$ ./autogen.sh

# build
$ ./configure
$ make
$ sudo make install
```

## Lambda用にパッケージング

```bash
$ cd ~
$ mkdir package
$ cd package

# Copy libraries
$ cp /usr/local/bin/tesseract .
$ mkdir lib
$ cd lib
$ cp /usr/local/lib/libtesseract.so.3 .
$ cp /usr/local/lib/liblept.so.5 .
$ cp /lib64/librt.so.1 .
$ cp /lib64/libz.so.1 .
$ cp /usr/lib64/libpng12.so.0 .
$ cp /usr/lib64/libjpeg.so.62 .
$ cp /usr/lib64/libtiff.so.5 .
$ cp /lib64/libpthread.so.0 .
$ cp /usr/lib64/libstdc++.so.6 .
$ cp /lib64/libm.so.6 .
$ cp /lib64/libgcc_s.so.1 .
$ cp /lib64/libc.so.6 .
$ cp /lib64/ld-linux-x86-64.so.2 .
$ cp /usr/lib64/libjbig.so.2.0 .

# Get trained data
$ cd ..
$ mkdir tessdata
$ cd tessdata
$ wget https://github.com/tesseract-ocr/tessdata/raw/master/eng.traineddata
$ wget https://github.com/tesseract-ocr/tessdata/raw/master/osd.traineddata

# Make config file
$ mkdir configs
$ echo 'tessedit_create_tsv 1' > tsv

$ cd ../..
$ zip -r package.zip package
```

これでLambdaのパッケージに `package` を閉じ込めてあげると使えるようになります！


## やってみた結果ｗｗｗｗｗｗ
草すみませんでした。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73113/e15768af-0925-e3a8-d208-65f6b95763ea.png)

こんな画像をあげた結果がこれ

```tsv
level	page_num	block_num	par_num	line_num	word_num	left	top	width	height	conf	text
1	1	0	0	0	0	0	0	1080	1920	-1	
2	1	1	0	0	0	29	11	1025	50	-1	
3	1	1	1	0	0	29	11	1025	50	-1	
4	1	1	1	1	0	29	11	1025	50	-1	
5	1	1	1	1	1	29	11	548	50	60	GnAﬂQﬂAA
5	1	1	1	1	2	640	15	167	43	58	X-IIZII"
5	1	1	1	1	3	899	14	155	44	89	l11:57
2	1	2	0	0	0	0	0	1080	76	-1	
3	1	2	1	0	0	0	0	1080	76	-1	
4	1	2	1	1	0	0	0	1080	76	-1	
5	1	2	1	1	1	0	0	1080	76	95	 
2	1	3	0	0	0	192	829	197	66	-1	
3	1	3	1	0	0	192	829	197	66	-1	
4	1	3	1	1	0	192	829	197	66	-1	
5	1	3	1	1	1	192	851	93	44	87	00
5	1	3	1	1	2	336	829	53	66	71	la
2	1	4	0	0	0	122	992	718	109	-1	
3	1	4	1	0	0	122	992	718	109	-1	
4	1	4	1	1	0	122	992	718	47	-1	
5	1	4	1	1	1	122	995	88	44	89	Sign
5	1	4	1	1	2	229	995	31	34	94	in
5	1	4	1	1	3	276	997	40	32	86	to
5	1	4	1	1	4	332	997	64	42	89	get
5	1	4	1	1	5	410	993	66	36	86	the
5	1	4	1	1	6	493	997	104	32	84	most
5	1	4	1	1	7	613	997	66	32	86	out
5	1	4	1	1	8	695	992	41	37	91	of
5	1	4	1	1	9	749	1003	91	36	93	your
4	1	4	1	2	0	122	1065	144	36	-1	
5	1	4	1	2	1	122	1065	144	36	87	device.
2	1	5	0	0	0	124	1269	312	46	-1	
3	1	5	1	0	0	124	1269	312	46	-1	
4	1	5	1	1	0	124	1269	312	46	-1	
5	1	5	1	1	1	124	1269	111	36	87	Email
5	1	5	1	1	2	253	1279	40	26	92	or
5	1	5	1	1	3	310	1269	126	46	89	phone
```


ソースはこんなん

```python
import requirements

from PIL import Image
import sys
import pyocr
import pyocr.builders

import urllib
import os
import subprocess
import base64
import json
import boto3

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LIB_DIR = os.path.join(SCRIPT_DIR, 'lib')
LANG_DIR = os.path.join(SCRIPT_DIR, 'tessdata')

def response(code, body):
    return {
        'statusCode': code,
        'headers': {
            'Access-Control-Allow-Origin': '*',
        },
        'body': json.dumps(body),
    }

def handler(event, context):
    # Get the bucket and object from the event
    try:
        tools = pyocr.get_available_tools()
        if len(tools) == 0:
            print("No OCR tool found")
            sys.exit(1)
        tool = tools[0]
        print("Will use tool '%s'" % (tool.get_name()))

        request = event['body']

        result_filepath = '/tmp/result'
        img_filepath = '/tmp/image.png'
        with open(img_filepath, 'wb') as fh:
            fh.write(base64.decodestring(request['template']))

        command = 'LD_LIBRARY={} TESSDATA_PREFIX={} {}/tesseract {} {} -l eng --oem 0  tsv'.format(
            LIB_DIR,
            SCRIPT_DIR,
            SCRIPT_DIR,
            img_filepath,
            result_filepath
        )
        print command

        try:
            output = subprocess.check_output(
                command,
                shell=True,
                stderr=subprocess.STDOUT
            )
            print(output)

            with open(result_filepath + '.tsv', 'rb') as fh:
                print(fh.read())
        except subprocess.CalledProcessError as e:
            return "except:: " + e.output

    except Exception as e:
        print(e)
        raise e
```

あとはGitHub上のserverless.ymlなり何なりを書き換えてご自由にお使いくださいませー。