手書き文字(日本語)のOCRを行い、Paragraph毎に画像分割してみた(Cloud Vision API & Lambda)
複数の手書き文字(日本語)が書かれている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に記載します。
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する権限を与える必要があります。
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バケットにアップロードしています。
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 |
分割結果
枝豆とじゃがいもは分割されていますが、両端が切れてしまっています。
さいごに
人間の認識能力ってすごいですね……。
参考
- Vision AI | ML から画像情報を引き出す | Cloud Vision API | Google Cloud
- Cloud Vision のドキュメント | Cloud Vision API | Google Cloud
- 画像内の手書き入力を検出する | Cloud Vision API | Google Cloud
- googleapis/python-vision
- Pillow — Pillow (PIL Fork) 7.2.0 documentation
- S3 — Boto3 Docs 1.15.16 documentation
- PIL/Pillow チートシート - Qiita
- Cloud Vision APIをLambdaで使って、手書き文字(日本語)のOCRをやってみた | Developers.IO