Goとtesseractで簡易OCRサーバを作る

【注意】
本記事で取り扱っているgosseractのバージョンは古いです。
現行バージョンの取り扱いについては公式ドキュメントを参照して下さい。
コメント欄も参照です。
otiai10さん、ありがとうございます。

Goで簡易OCRサーバを作る
=======================

* [Go 1.5.1](https://golang.org/)
* [tesseract-ocr 3.0.3](https://github.com/tesseract-ocr)
* [gosseract](https://github.com/otiai10/gosseract)
* [cutter](https://github.com/oliamb/cutter)
* [gorilla/mux](https://github.com/gorilla/mux)

はじめに
--------------

OCRサーバと言えば[open-ocr](https://github.com/tleyden/open-ocr)がありますが、遊びで使うにはオーバースペック感が否めなかったので簡易的なOCRサーバをGoとDockerで構築してみました。

構成
--------------

OCRソフトウェアには定評がある&Goのバインディングがあるtesseract-ocrを採用。
Pure Goでは無いので実行環境にはtesseract-ocr本体が必要になります。
しかし、試しに動かしたいだけでtesseract-ocrを丸ごとインストールするのはしんどいので、Docker内にtesseract-ocrとGo製のフロントエンドを閉じ込める方式にします。

フロントエンドはnet/httpでHTTPサーバにして、multipartで認識対象のファイルをPOSTして認識結果のテキストを返すようにします。

Docker
--------------

Dockerfile

```
FROM ubuntu:14.04

RUN apt-get update &&\
    apt-get upgrade -y

RUN apt-get -q -y install libleptonica-dev
RUN apt-get -q -y install libtesseract3 libtesseract-dev
RUN apt-get -q -y install tesseract-ocr
RUN apt-get -q -y install tesseract-ocr-eng tesseract-ocr-ara tesseract-ocr-bel tesseract-ocr-ben tesseract-ocr-bul tesseract-ocr-ces tesseract-ocr-dan tesseract-ocr-deu tesseract-ocr-ell tesseract-ocr-fin tesseract-ocr-fra tesseract-ocr-heb tesseract-ocr-hin tesseract-ocr-ind tesseract-ocr-isl tesseract-ocr-ita tesseract-ocr-jpn tesseract-ocr-kor tesseract-ocr-nld tesseract-ocr-nor tesseract-ocr-pol tesseract-ocr-por tesseract-ocr-ron tesseract-ocr-rus tesseract-ocr-spa tesseract-ocr-swe tesseract-ocr-tha tesseract-ocr-tur tesseract-ocr-ukr tesseract-ocr-vie tesseract-ocr-chi-sim tesseract-ocr-chi-tra

RUN  apt-get clean && \
     rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

ADD gocr /opt/gocr

ENTRYPOINT ["/opt/gocr"]
```

apt-getでインストールできるパッケージは最新版じゃないんですが、とりあえず試したかっただけなのでここも手抜き。
構成を詰めるなら手動インストールの手順を書けばいいですね。

`ADD gocr /opt/gocr`の部分で指定しているのが後述するGoアプリケーションのビルド済み実行ファイルです。

Go
--------------

main.go

```go
package main

import (
	"fmt"
	"net/http"
	"strconv"
	"flag"
	"image"
	_ "image/png"
	_ "image/jpeg"
	_ "image/gif"
	"github.com/gorilla/mux"
	"github.com/oliamb/cutter"
	"github.com/otiai10/gosseract"
)

func main() {
	bind := flag.String("bind", "127.0.0.1:8998", "bind")
	flag.Parse()
	WebApp(Config{
		Bind: *bind,
	})
}

type Config struct {
	Bind string
}

func WebApp(conf Config) {
	router := mux.NewRouter()
	router.HandleFunc("/read", readHandler).Methods("POST")

	http.ListenAndServe(
		conf.Bind,
		router,
	)
}

// 参考: https://astaxie.gitbooks.io/build-web-application-with-golang/content/en/04.5.html
func readHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")

	x, _ := strconv.Atoi(r.FormValue("x"))
	y, _ := strconv.Atoi(r.FormValue("y"))
	width, _ := strconv.Atoi(r.FormValue("width"))
	height, _ := strconv.Atoi(r.FormValue("height"))

	r.ParseMultipartForm(32 << 20)
	file, _, err := r.FormFile("image")
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	defer file.Close()

	img, _, err := image.Decode(file)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}

	croppedImage, err := cutter.Crop(img, cutter.Config{
		Width:  width,
		Height: height,
		Anchor: image.Point{
			X: x,
			Y: y,
		},
	})

	text, err := read(croppedImage)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}

	fmt.Fprintln(w, text)
}

func read(img image.Image) (string, error) {
	c, _ := gosseract.NewClient()
	c.Image(img)
	return c.Out()
}
```

読み込みたいファイルに画像と文字が混在していることを考慮して、読み取る範囲を指定してその部分だけをOCRにかけるようにしました。
画像には文字しか入っていない！という場合はもっとシンプルになりますね。

ビルドするのにもtesseract-ocrが必要ですがちょっと試すだけで開発環境にインストールするのも嫌なので、実行環境と同様にビルド用のDockerイメージを用意してしまうのが簡単です。

実行してみる
--------------

適当な画像を用意して読み込んでみます。

english.png
![english.png](https://qiita-image-store.s3.amazonaws.com/0/36061/1a74544c-a1ae-8d7a-c134-778e1e748e67.png)

```
% docker run -p 127.0.0.1:8998:8998 -d fumiz/gocr -bind=0.0.0.0:8998
% http -f POST http://$(docker-machine ip default):8998/read x=20 y=295 width=281 height=98 image@~/temp/english.png
HTTP/1.1 200 OK
Content-Length: 96
Content-Type: text/plain; charset=utf-8
Date: Sun, 25 Oct 2015 09:27:37 GMT

This \s a pen

But, ‘5 he a pen?

Yes, he Is Tom as wen es the pen.
My hometown l5 Mombasa.
```

特に何もオプションをいじっていない割にはまあまあでしょうか。

注意点
--------------

* [gosseract](https://github.com/otiai10/gosseract)の「画像を受けて直接読み取る」APIは内部で一時的な画像ファイルを作成していますが、読み取り後に自動的に削除するようにはなっていないのでゴミがだんだんたまっていきます。あくまで実験用ということで……
  * https://github.com/otiai10/gosseract/blob/master/client.go#L63
* 英語でしか試していません。日本語を読み取るようにすることもできる筈ではありますが……

感想
--------------

簡易OCRサーバは簡易なので簡単に作れたけれども、精度を上げたり言語を切り替えられるようにしようと思うと大変そう。