goによるOCRエンジン実行のまとめ

会計フリー周りのエンジニアをしているよーだ(@rtryoda)です。この記事は freee Developers Advent Calendar 2019 の11日目です。最近各ベンダーやOSSのOCRエンジンをgoで触る機会があったので、実行方法と結果をまとめました。

OCRとは

OCRとは光学的文字認識(Optical Character Recognition)のことで、画像などに記されている文字を読み取りテキストデータに変換することです。例えば以下のような画像に対してOCRを実行すると"あいうえお 12345"と認識されることを期待します。

f:id:rtryoda:20191210044243j:plain

※各OCRの実行にはこちらの画像を使用します。

今回試すOCRエンジン一覧

OCRエンジン 日本語対応 クライアントライブラリ(go)
Google Cloud Vision API googleapis/google-cloud-go
Azure Computer Vision -
Amazon Rekognition Image × aws/aws-sdk-go
Tesseract otiai10/gosseract

ざっくり触ってみた感想

  • Google Cloud Vision API
    • SDKもあってドキュメントも充実していて触りやすい
    • 日本語に対応していて精度も高くて驚いた
  • Azure Computer Vision
    • SDKがなくREST APIで実行する必要があり、goでmaltipartで画像送るのが少し面倒くさい
    • 日本語に対応しているが精度もGoogle Cloud Vision APIやTesseractには及ばず
    • 正確な数値は計測していないが4つの中では一番レスポンスが早かった
  • Amazon Rekognition Image
    • goのSDKがあり他のAWSの機能と使い方が似ていて手軽に触れた
    • 4つの中では信頼度やポリゴンなど一番詳細な情報が返ってくる
    • 日本語対応していないのが厳しい
  • Tesseract
    • ローカルにインストールすれば手軽に触れて便利
    • オプション多かったり独自の学習ができて拡張性が高い
    • 返り値の信頼度を見て精度高められないか検討したが、自信満々に間違えることもあり信頼度はあまり使えなかった
    • gosseractのインターフェースがよく出来ていて実装は一番シンプルになった

実行方法

Google Cloud Vision API

Google Cloud Vision APIはgoのSDKが公開されているのでこちらを利用します。

$ go get -u cloud.google.com/go/vision/apiv1

また事前に認証キーが含まれるJSONファイルを取得しファイルのパスを環境変数に指定します。

$ export GOOGLE_APPLICATION_CREDENTIALS=xxxxx

実装は公式サンプルを参考にしました。

(各OCRの実装はエラー処理を省いています)

package main

import (
    vision "cloud.google.com/go/vision/apiv1"
    "context"
    "fmt"
    "os"
)

func main() {
    // クライアントの作成
    ctx := context.Background()
    client, _ := vision.NewImageAnnotatorClient(ctx)

    // 画像の読み込み
    file, _ := os.Open("sample.jpg")
    defer file.Close()
    image, _ := vision.NewImageFromReader(file)

    // 実行
    annotations, _ := client.DetectTexts(ctx, image, nil, 10)

    for _, annotation := range annotations {
        fmt.Printf(
            "locale:%s, description:%s, bounding_poly:%+v\n",
            annotation.Locale,
            annotation.Description,
            annotation.BoundingPoly,
        )
    }
}

実行結果

locale:ja, description:あいうえお
12345
, bounding_poly:vertices:<x:22 y:19 > vertices:<x:314 y:19 > vertices:<x:314 y:35 > vertices:<x:22 y:35 >
locale:, description:あいうえお, bounding_poly:vertices:<x:22 y:19 > vertices:<x:115 y:19 > vertices:<x:115 y:35 > vertices:<x:22 y:35 >
locale:, description:12345, bounding_poly:vertices:<x:263 y:20 > vertices:<x:314 y:21 > vertices:<x:314 y:35 > vertices:<x:263 y:34 >

戻り値のEntityAnnotationのメンバにはConfidence(信頼度)がありますが、DetectTextでは値が入らないことに注意が必要です。

Azure Computer Vision

Azure Computer VisionはgoのSDKがないのでREST APIを利用します。まず、認証キーとエンドポイントを取得し環境変数に設定します。

$ export COMPUTER_VISION_SUBSCRIPTION_KEY=xxxxx
$ export COMPUTER_VISION_ENDPOINT=xxxxx

実装は公式サンプルを参考にしました。

package main

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "mime/multipart"
    "net/http"
    "os"
)

func main() {
    // 環境変数の読み込み
    subscriptionKey := os.Getenv("COMPUTER_VISION_SUBSCRIPTION_KEY")
    endpoint := os.Getenv("COMPUTER_VISION_ENDPOINT")

    // 画像の読み込み
    var buf bytes.Buffer
    w := multipart.NewWriter(&buf)
    fw, _ := w.CreateFormFile("data", "image")
    file, _ := os.Open("sample.jpg")
    io.Copy(fw, file)
    file.Close()
    w.Close()

    // REST APIの実行
    uri := endpoint + "/vision/v2.1/ocr?language=unk&detectOrientation=true"
    req, _ := http.NewRequest("POST", uri, &buf)
    req.Header.Add("Content-Type", w.FormDataContentType())
    req.Header.Add("Ocp-Apim-Subscription-Key", subscriptionKey)
    client := &http.Client{}
    resp, _ := client.Do(req)
    defer resp.Body.Close()

    data, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(data))
}

実行結果

{
  "language": "ja",
  "textAngle": 0,
  "orientation": "Up",
  "regions": [
    {
      "boundingBox": "22,19,95,17",
      "lines": [
        {
          "boundingBox": "22,19,95,17",
          "words": [
            { "boundingBox": "22,19,16,17", "text": "" },
            { "boundingBox": "42,21,16,14", "text": "" },
            { "boundingBox": "62,19,14,17", "text": "" },
            { "boundingBox": "81,19,17,17", "text": "" },
            { "boundingBox": "100,19,17,16", "text": "" }
          ]
        }
      ]
        },
        {
            "boundingBox": "262,20,53,16",
            "lines": [
                {
                    "boundingBox": "262,20,53,16",
                    "words": [
                        { "boundingBox": "262,20,20,15", "text": "12" },
                        { "boundingBox": "305,21,10,15", "text": "5" }
                    ]
                }
            ]
        }
    ]
}

Azure Computer VisionもConfidenceを返しません。また、REST APIなので実行結果をgoで扱う場合はJSON文字列を構造体に変換する必要があります。

var result struct {
    Language    string
    Orientation string
    Regions     []struct {
        BoundingBox string
        Lines       []struct {
            BoundingBox string
            Words       []struct {
                BoundingBox string
                Text        string
            }
        }
    }
}
json.Unmarshal(data, &result)

Amazon Rekognition Image

Amazon Rekognition ImageはgoのSDKが公開されているのでこちらを利用します。

$ go get -u github.com/aws/aws-sdk-go

また、認証キーを環境変数に設定する必要があります。

$ export AWS_ACCESS_KEY_ID=xxxx
$ export AWS_SECRET_ACCESS_KEY=xxx

実装はGithubのREADMEを参考にしました。

package main

import (
    "fmt"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/rekognition"
    "io/ioutil"
    "os"
)

func main() {
    // ファイル読み込み
    file, _ := os.Open("sample.jpg")
    defer file.Close()
    bytes, _ := ioutil.ReadAll(file)

    // セッション、クライアント、インプットの作成
    awsSession := session.Must(session.NewSession())
    svc := rekognition.New(awsSession, aws.NewConfig().WithRegion("ap-northeast-1"))
    input := &rekognition.DetectTextInput{Image: &rekognition.Image{Bytes: bytes}}

    // Rekognitionの実行
    output, err := svc.DetectText(input)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%+v\n", output)
}

実行結果

{
  TextDetections: [
    {
      Confidence: 58.572227478027344,
      DetectedText: "$y",
      Geometry: {
        BoundingBox: { Height: 0.42103537917137146, Left: 0.07299134880304337, Top: 0.24968594312667847, Width: 0.14152440428733826 },
        Polygon: [
          { X: 0.07299134880304337, Y: 0.24968594312667847 },
          { X: 0.21451574563980103, Y: 0.421069860458374 },
          { X: 0.1993957757949829, Y: 0.8421052694320679 },
          { X: 0.05787137523293495, Y: 0.6707213521003723 }
        ]
      },
      Id: 0,
      Type: "LINE"
    },
    {
      Confidence: 99.96682739257812,
      DetectedText: "12345",
      Geometry: {
        BoundingBox: { Height: 0.2631579041481018, Left: 0.7885196208953857, Top: 0.3333333432674408, Width: 0.16012084484100342 },
        Polygon: [
          { X: 0.7885196208953857, Y: 0.3333333432674408 },
          { X: 0.9486404657363892, Y: 0.3333333432674408 },
          { X: 0.9486404657363892, Y: 0.5964912176132202 },
          { X: 0.7885196208953857, Y: 0.5964912176132202 }
        ]
      },
      Id: 1,
      Type: "LINE"
    },
    {
      Confidence: 58.572227478027344,
      DetectedText: "$y",
      Geometry: {
        BoundingBox: { Height: 0.4213235080242157, Left: 0.07250755280256271, Top: 0.2631579041481018, Width: 0.21235120296478271 },
        Polygon: [
          { X: 0.07250755280256271, Y: 0.2631579041481018 },
          { X: 0.2145015150308609, Y: 0.42105263471603394 },
          { X: 0.1993957757949829, Y: 0.8421052694320679 },
          { X: 0.06042296066880226, Y: 0.6666666865348816 }
        ]
      },
      Id: 2,
      ParentId: 0,
      Type: "WORD"
    },
    {
      Confidence: 99.96682739257812,
      DetectedText: "12345",
      Geometry: {
        BoundingBox: { Height: 0.2631579041481018, Left: 0.7885196208953857, Top: 0.3333333432674408, Width: 0.16012084484100342 },
        Polygon: [
          { X: 0.7885196208953857, Y: 0.3333333432674408 },
          { X: 0.9486404657363892, Y: 0.3333333432674408 },
          { X: 0.9486404657363892, Y: 0.5964912176132202 },
          { X: 0.7885196208953857, Y: 0.5964912176132202 }
        ]
      },
      Id: 3,
      ParentId: 1,
      Type: "WORD"
    }
  ]
}

Amazon Rekognition ImageはConfidenceを返しますが、日本語非対応のため日本語が読み取れていないことが分かります。

Tesseract

TesseractはオープンソースのOCRエンジンです。Tesseractにはgo向けのラッパーである otiai10/gosseract があるのでこちらを利用します。まずは、tesseractのインストールをします。

$ brew install tesseract

デフォルトでは英語データしか用意されていないので、日本語データを追加する必要があります。Githubのリポジトリからjpn.traineddataをダウンロードし、/usr/local/share/tessdataに置きます。以下のコマンドを実行し、jpnが表示されれば日本語データの準備完了です。

$ tesseract --list-langs
List of available languages (6):
eng
jpn

次にgosseractのインストールを行います。

$ go get -t github.com/otiai10/gosseract

実装はgosseractのREADMEを参考にしました。

package main

import (
    "fmt"
    "github.com/otiai10/gosseract"
)

func main() {
    // クライアント作成
    client := gosseract.NewClient()
    defer client.Close()

    // クライアント設定
    client.SetLanguage("eng", "jpn") //jpnを指定するにはjpn.traineddataが必要
    client.SetImage("sample.jpg")

    // textのみ取得
    text, _ := client.Text()
    fmt.Printf("%s\n", text)

    // Level(BLOCK, PARA, TEXTLINE, WORD, SYMBOL)を指定して詳細を取得
    boxes, _ := client.GetBoundingBoxes(gosseract.RIL_SYMBOL)
    for _, box := range boxes {
        fmt.Printf("%+v\n", box)
    }
}

実行結果

あ い う え お ⑫③④⑤
{Box:(22,19)-(48,36) Word:あ Confidence:99.56348419189453 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(53,22)-(58,32) Word:い Confidence:99.56478881835938 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(62,19)-(76,36) Word:う Confidence:99.54214477539062 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(81,19)-(97,35) Word:え Confidence:99.55460357666016 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(100,19)-(116,35) Word:お Confidence:99.29891204833984 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(263,21)-(282,35) Word:⑫ Confidence:99.04163360595703 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(274,21)-(292,35) Word:③ Confidence:98.84930419921875 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(283,21)-(303,35) Word:④ Confidence:99.01544952392578 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}
{Box:(305,21)-(314,35) Word:⑤ Confidence:98.99661254882812 BlockNum:0 ParNum:0 LineNum:0 WordNum:0}

Textメソッドで文字列のみを取得する他に、GetBoundingBoxesメソッドで引数に指定したレベルの詳細を取得することもできます。また、今回の例では12として認識していますが、ホワイトリストやブラックリストを指定することで認識文字を制限できます。

client.SetBlacklist("①②③④⑤⑫")
text, _ := client.Text()
fmt.Printf("%s\n", text)
// 実行結果
// あ い う え お 12345

他にもTesseractはフォントを指定して再学習をさせたりと拡張性が高いことが特徴です。

おわりに

この記事ではサンプル画像のみの実行でしたが、私の担当している業務で扱う画像を想定した検証ではGoogle Cloud Vision APIが日本語含め飛び抜けて認識精度が良い結果になりました。

また、Azure以外はgo向けのクライアントライブラリがありgoの人気を改めて実感しました。goでOCRを実行する際の参考にしていただければ幸いです。

明日はApple製品をこよなく愛する@_kemuridamaです!よろしくお願いします!