手書き文字(日本語)のOCRを行い、Paragraph毎に画像分割してみた(Cloud Vision API & Lambda)

GCPのCloud Vision APIを使えば、日本語のOCR(光学文字認識)ができます。 そこで、LambdaでCloud Vision APIを使って、手書き文字(日本語)のOCRを行い、Paragraph毎に画像を切り出してみました。
2020.10.13

複数の手書き文字(日本語)が書かれている1つの画像があるとします。 この画像について、Paragraph毎に画像を分割してみました。

S3バケットに画像を置くとLambdaが起動し、Lambda内でGCPのCloud Vision APIを使ってOCRを行います。 OCR結果に従ってParagraph毎に画像を切り出し、S3バケットに保存します。

なお、画像の切り出しにはPillowを使います。

おすすめの方

  • S3にオブジェクトを置くと、SNSトピックを発行する方法を知りたい方
  • LambdaでGCPのCloud Vision APIを使ってみたい方
  • LambdaからS3にファイルをアップロードしたい方
  • AWS SAMを使ってみたい方

GCPのプロジェクトを作成する

こちらにアクセスして、プロジェクトの設定を行います。 GCPコンソールからプロジェクト作成を行ってもOKです。

そのままCloud Vision APIを有効にします。

続いてJSONのプライベートキーをダウンロードしておきます。

サーバーレスアプリを作成する

SAM Init

sam init \
    --runtime python3.7 \
    --name GCP-OCR-Crop-Sample \
    --app-template hello-world

VisionクライアントライブラリとPillowを使う

GCPのVisionクライアントライブラリとPillowを使うため、requirements.txtに記載します。

requirements.txt

google-cloud-vision
pillow

認証情報JSONファイルを格納する

GCPのプロジェクト作成時にダウンロードした認証情報(JSONファイル)をLambdaハンドラーと同じ場所に格納します。

├── hello_world
│   ├── __init__.py
│   ├── app.py
│   ├── gcp.json
│   └── requirements.txt
└── template.yaml

SAMテンプレートファイル

認証情報(JSONファイル)のファイル位置をLambdaの環境変数で設定しています。また、S3バケットに対して、SNSトピックをPublishする権限を与える必要があります。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GCP-OCR-Crop-Sample

Resources:
  ImageSourceBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: cm-fujii-genki-ocr-crop-src-sample-bucket
      NotificationConfiguration:
        TopicConfigurations:
          - Event: s3:ObjectCreated:*
            Topic: !Ref OcrNotifyTopic

  ImageOutputBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: cm-fujii-genki-ocr-crop-output-sample-bucket

  OcrNotifyTopic:
    Type: AWS::SNS::Topic

  OcrNotifyTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref OcrNotifyTopic
      PolicyDocument:
        Id: !Ref OcrNotifyTopic
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: s3.amazonaws.com
            Action: SNS:Publish
            Resource: !Ref OcrNotifyTopic
            Condition:
              ArnLike:
                aws:SourceArn: arn:aws:s3:::cm-fujii-genki-ocr-crop-src-sample-bucket

  OcrSampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 30
      MemorySize: 3008
      Policies:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      Environment:
        Variables:
          GOOGLE_APPLICATION_CREDENTIALS: gcp.json
          OUTPUT_S3_BUCKET_NAME: !Ref ImageOutputBucket
      Events:
        S3Event:
          Type: SNS
          Properties:
            Topic: !Ref OcrNotifyTopic

  OcrSampleFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${OcrSampleFunction}

Lambdaコード

S3バケットから画像ファイルをダウンロードし、GCPのCloud Vision APIでOCRを行い、Paragraph領域を切り取ってS3バケットにアップロードしています。

app.py

import json
import os
import boto3
from PIL import Image

s3 = boto3.client('s3')

OUTPUT_S3_BUCKET_NAME = os.environ['OUTPUT_S3_BUCKET_NAME']

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    s3_event = message['Records'][0]['s3']

    bucket_name = s3_event['bucket']['name']
    key_name = s3_event['object']['key']

    print(f'target: {bucket_name}, {key_name}')

    tmp_file_path = f'/tmp/{key_name}'

    s3.download_file(bucket_name, key_name, tmp_file_path)

    with open(tmp_file_path, 'rb') as f:
        body = f.read()

    paragraph_box = detect_document(body)

    crop_and_upload(tmp_file_path, paragraph_box)


def crop_and_upload(file_path, paragraph_box):
    image = Image.open(file_path)

    for index, bound in enumerate(paragraph_box):
        # 指定領域を切り取る(トリミング)
        crop_image = image.crop((
            bound.vertices[0].x, bound.vertices[0].y,   # 左上の座標
            bound.vertices[2].x, bound.vertices[2].y,   # 右下の座標
        ))
        file_name, extension = os.path.splitext(file_path)
        output_file_path = f'{file_name}_crop_{index:03}{extension}'
        crop_image.save(output_file_path)

        s3.upload_file(
            output_file_path,
            OUTPUT_S3_BUCKET_NAME,
            os.path.basename(output_file_path)
        )


def detect_document(body):
    # https://cloud.google.com/vision/docs/handwriting
    from google.cloud import vision
    client = vision.ImageAnnotatorClient()
    image = vision.Image(content=body)

    response = client.document_text_detection(
        image=image,
        image_context={'language_hints': ['ja']}
    )

    paragraph_box = []

    for page in response.full_text_annotation.pages:
        for block in page.blocks:
            print('+ Block confidence: {}\n'.format(block.confidence))

            for paragraph in block.paragraphs:
                paragraph_box.append(paragraph.bounding_box)

                print('-- Paragraph confidence: {}'.format(
                    paragraph.confidence))

                for word in paragraph.words:
                    word_text = ''.join([
                        symbol.text for symbol in word.symbols
                    ])
                    print('**** Word text: {} (confidence: {})'.format(
                        word_text, word.confidence))

                    for symbol in word.symbols:
                        print('>>>>>> Symbol: {} (confidence: {})'.format(
                            symbol.text, symbol.confidence))

    if response.error.message:
        raise Exception(
            '{}\nFor more info on error messages, check: '
            'https://cloud.google.com/apis/design/errors'.format(
                response.error.message))

    return paragraph_box

デプロイ

sam build --use-container

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name GCP-OCR-Crop-Sample-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

さっそく動かす

iPhoneのメモアプリで手書きし、3種類の画像を用意しました。

にんじん・大根

S3にアップロードした画像

以前も使った画像ですが、画像サイズ(余白部分)が異なります。

OCR結果

にんじんさんが……。

分割結果

大根部分が少し欠けました。

牛肉・はくさい・れんこん

S3にアップロードした画像

OCR結果

はくさい部分が惜しいですね。

分割結果

牛肉とはくさいが同じParagraphと認識されました。意図的に改行時の空白を大きくすると、3分割になるかもしれません。

枝豆・じゃがいも・ブロッコリー・レタス

S3にアップロードした画像

OCR結果

じゃがいもとブロッコリーが駄目でした。アルファベットに似ている縦棒?あたりが怪しいのでしょうか。 それにしても、以下は「言われてみれば……」と少し納得です。

手書き文字 OCR結果
L'
th
7
ロッ Diy

分割結果

枝豆とじゃがいもは分割されていますが、両端が切れてしまっています。

さいごに

人間の認識能力ってすごいですね……。

参考